mirror of
https://github.com/TheRealRetro/mupen64plus-ps5.git
synced 2026-10-27 06:02:06 +01:00
Compare commits
1
Commits
00.006.002
...
00.006.003
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b75d1130e3 |
No files matched your search
@@ -23,3 +23,6 @@ github-token.txt
|
||||
*.swp
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Local test builds
|
||||
test-updater/
|
||||
@@ -15,7 +15,7 @@ Title ID `PPSA99064`. No games are included: use ROMs you dumped yourself.
|
||||
Requirements: a jailbroken PS5 with **kstuff** (or kstuff-lite), **ShadowMountPlus**, and an **ELF loader**
|
||||
running (elfldr, etaHEN or PS5 Payload Manager's: the usual setup).
|
||||
|
||||
1. Download `PPSA99064.zip` from the [latest release](../../releases/latest).
|
||||
1. Download `Mupen64PlusPS5.zip` from the [latest release](../../releases/latest).
|
||||
2. Copy the `PPSA99064` folder to `/data/homebrew/` on the console (FTP...), or to `homebrew/` on an exFAT
|
||||
USB drive. ShadowMountPlus adds **Mupen64Plus PS5** to the home screen.
|
||||
3. Put your ROMs (`.z64 .n64 .v64 .rom`, zipped or not) in `/data/mupen64plus/roms`, or in `mupen64plus/roms`
|
||||
@@ -27,9 +27,14 @@ which lets it out of its sandbox (`/data`, USB drives, executable memory for the
|
||||
## Updates
|
||||
|
||||
The app checks this repository's latest release each time it starts. When a newer one exists it downloads
|
||||
`PPSA99064.zip`, checks it against the SHA-256 GitHub publishes for it, and asks:
|
||||
**✕ Update now / ○ Later**. On ✕ it writes the new files over its own folder and restarts. Saves, states,
|
||||
covers and settings in `/data/mupen64plus` are kept. **Settings → Check for updates** turns the question off.
|
||||
`Mupen64PlusPS5.zip`, checks it against the SHA-256 GitHub publishes for it, and asks:
|
||||
**✕ Update now / ○ Later**. On ✕ it unpacks the update and closes; a background helper installs it, and a
|
||||
notification says when to open the app again. Saves, states, covers and settings in `/data/mupen64plus` are
|
||||
kept. **Settings → Check for updates** turns the question off.
|
||||
|
||||
**On 0.6.0, 0.6.1 or 0.6.2?** Their updater is broken (the updated app wouldn't start), so they don't see
|
||||
newer releases. Install 0.6.3 or newer by hand once: delete `/data/homebrew/PPSA99064`, then copy the
|
||||
`PPSA99064` folder from `Mupen64PlusPS5.zip`. Updates after that work from inside the app.
|
||||
|
||||
## Compatibility
|
||||
|
||||
@@ -47,7 +52,7 @@ For a crash or a bug, please [open an issue](../../issues) and attach `/data/mup
|
||||
## Building and releasing (Windows + WSL Ubuntu 24.04)
|
||||
|
||||
```
|
||||
build-native.bat build-native\PPSA99064\ + PPSA99064.zip (+ .debug.elf)
|
||||
build-native.bat build-native\PPSA99064\ + Mupen64PlusPS5.zip (+ .debug.elf)
|
||||
build-native.bat Ffpfsc also a compressed .ffpfsc image
|
||||
build-native.bat Clean from scratch (nothing reused from earlier builds)
|
||||
release.bat clean build, then publish a GitHub release
|
||||
@@ -58,7 +63,7 @@ Releasing:
|
||||
1. Raise `VERSION` in `ps5/Makefile`, and set the matching `contentVersion` in `ps5/app/sce_sys/param.json`
|
||||
(0.7.0 → `00.007.000`; the build stops if they differ). That number is also the release tag.
|
||||
2. Write `release-notes/<contentVersion>.md`: a few short lines, shown on the console in the update question.
|
||||
3. Commit and push, then run `release.bat`. It builds, creates the release with `PPSA99064.zip`, and checks
|
||||
3. Commit and push, then run `release.bat`. It builds, creates the release with `Mupen64PlusPS5.zip`, and checks
|
||||
GitHub's digest against the local zip.
|
||||
|
||||
Never reuse or lower a version.
|
||||
|
||||
+17
-12
@@ -4,7 +4,7 @@
|
||||
# included) and stays running as the helper that lets the app reach /data
|
||||
# make app -> build/app/$(TITLE_ID)/ the app folder on its own (eboot.bin, sce_module/, sce_sys/)
|
||||
# make host -> build/host/ Linux builds of the same code (PS5 calls faked by host/sce_host.cpp)
|
||||
# make native -> ../build-native/: $(TITLE_ID)/ (the app folder), $(TITLE_ID).zip (the release),
|
||||
# make native -> ../build-native/: $(TITLE_ID)/ (the app folder), $(RELEASE_ZIP) (the release),
|
||||
# $(TITLE_ID).debug.elf, and $(TITLE_ID).ffpfsc with FORMAT=Ffpfsc -- the layout of the Homebrew
|
||||
# Browser's build-native.bat (../build-native.bat runs this)
|
||||
# make dist -> make native + build/dist/Mupen64PS5-v$(VERSION).elf (the optional installer) and
|
||||
@@ -32,13 +32,17 @@
|
||||
PS5_PAYLOAD_SDK ?= /opt/ps5-payload-sdk
|
||||
PS5_HOST ?= ps5
|
||||
PS5_PORT ?= 9021
|
||||
VERSION ?= 0.6.2
|
||||
VERSION ?= 0.6.3
|
||||
# param.json's contentVersion, NN.NNN.NNN (0.6.1 -> 00.006.001): the release tag, as in the Homebrew Browser; it
|
||||
# must also be written in app/sce_sys/param.json (checked below)
|
||||
CONTENT_VERSION := $(shell echo $(VERSION) | awk -F. '{printf "%02d.%03d.%03d", $$1, $$2, $$3}')
|
||||
TITLE_ID ?= PPSA99064
|
||||
# where the app looks for updates (fe_update.h) and `make release` publishes
|
||||
GITHUB_REPO ?= TheRealRetro/mupen64plus-ps5
|
||||
# The release zip's name, which the app's updater looks for. Up to 0.6.2 it was $(TITLE_ID).zip, but those
|
||||
# versions' updater wrote files without execute permission and left the app unable to start: under this name
|
||||
# they never find a newer release, so they can't break themselves (their users install 0.6.3 by hand once).
|
||||
RELEASE_ZIP := Mupen64PlusPS5.zip
|
||||
APP_OUT := build/app/$(TITLE_ID)
|
||||
|
||||
M64 := ../mupen64plus-core
|
||||
@@ -94,13 +98,13 @@ EMU_SRC := coreorbis/main-boot.cpp \
|
||||
coreorbis/orbis-shims/orbis_paths.cpp coreorbis/orbis-shims/ProsperoNotify.cpp \
|
||||
coreorbis/orbis-shims/ProsperoVideo.cpp coreorbis/orbis-shims/ProsperoAudio.cpp \
|
||||
coreorbis/orbis-shims/ProsperoInput.cpp coreorbis/orbis-shims/ProsperoJailbreak.cpp \
|
||||
coreorbis/orbis-shims/ProsperoCrash.cpp \
|
||||
coreorbis/orbis-shims/ProsperoCrash.cpp coreorbis/orbis-shims/ProsperoUpdateJob.cpp \
|
||||
frontend/fe_emu.cpp frontend/fe_menu.cpp frontend/fe_settings.cpp frontend/fe_text.cpp \
|
||||
frontend/fe_games.cpp frontend/fe_http.cpp frontend/fe_covers.cpp frontend/fe_shelf.cpp \
|
||||
frontend/fe_prefetch.cpp frontend/fe_update.cpp
|
||||
# the payloads: the helper, and the installer (= helper + install)
|
||||
HELPER_SRC := coreorbis/orbis-shims/orbis_paths.cpp coreorbis/orbis-shims/ProsperoNotify.cpp \
|
||||
coreorbis/orbis-shims/ProsperoHelper.cpp
|
||||
coreorbis/orbis-shims/ProsperoHelper.cpp coreorbis/orbis-shims/ProsperoUpdateJob.cpp
|
||||
INSTALLER_SRC := installer/installer_main.cpp $(HELPER_SRC) coreorbis/orbis-shims/ProsperoInstall.cpp
|
||||
|
||||
ZLIB_SRC := $(wildcard $(ZLIB)/*.c)
|
||||
@@ -108,7 +112,8 @@ ZLIB_SRC := $(wildcard $(ZLIB)/*.c)
|
||||
INCS := -I$(M64SRC) -I$(M64)/subprojects/minizip -I$(ZLIB) \
|
||||
-Icoreorbis/include-orbis -Icoreorbis/orbis-shims -Ifrontend -In64 -In64/core
|
||||
DEFS := -DN64PS5_VERSION='"$(VERSION)"' -DN64PS5_TITLE_ID='"$(TITLE_ID)"' \
|
||||
-DN64PS5_CONTENT_VERSION='"$(CONTENT_VERSION)"' -DN64PS5_GITHUB_REPO='"$(GITHUB_REPO)"'
|
||||
-DN64PS5_CONTENT_VERSION='"$(CONTENT_VERSION)"' -DN64PS5_GITHUB_REPO='"$(GITHUB_REPO)"' \
|
||||
-DN64PS5_RELEASE_ZIP='"$(RELEASE_ZIP)"'
|
||||
PORT_WARN := -Wall -Wextra -Werror -Wno-unused-parameter -Wno-missing-field-initializers
|
||||
|
||||
# generated: the game-name table (from the core's ROM catalog) built into fe_games.o
|
||||
@@ -250,29 +255,29 @@ strip: $(PS5_OUT)/Mupen64PS5-stripped.elf
|
||||
# The release, in the Homebrew Browser's layout (its native/build-native.sh), in ../build-native/:
|
||||
# $(TITLE_ID)/ the app folder: copied to /data/homebrew/ (or a USB drive's homebrew/), ShadowMountPlus
|
||||
# lists it
|
||||
# $(TITLE_ID).zip the release artifact: just $(TITLE_ID)/ (the browser installs a release's
|
||||
# $(TITLE_ID).zip; the tag is the contentVersion, $(CONTENT_VERSION))
|
||||
# $(RELEASE_ZIP) the release artifact: just $(TITLE_ID)/ (the browser installs a release's
|
||||
# $(RELEASE_ZIP); the tag is the contentVersion, $(CONTENT_VERSION))
|
||||
# $(TITLE_ID).debug.elf eboot.bin's program with its symbols, for reading crash reports
|
||||
# $(TITLE_ID).ffpfsc with FORMAT=Ffpfsc: a compressed image of the folder (the boilerplate's mkpfs)
|
||||
# release-assets/$(CONTENT_VERSION)/$(TITLE_ID).zip a copy kept per version
|
||||
# release-assets/$(CONTENT_VERSION)/$(RELEASE_ZIP) a copy kept per version
|
||||
NATIVE_OUT := ../build-native
|
||||
FORMAT ?= Folder
|
||||
PS5_NATIVE_BOILERPLATE ?= /root/ps5-native
|
||||
native: $(APP_FILES) build/app/mupen64plus-pie.elf
|
||||
@mkdir -p "$(abspath $(NATIVE_OUT))"
|
||||
rm -rf "$(abspath $(NATIVE_OUT))/$(TITLE_ID)" "$(abspath $(NATIVE_OUT))/$(TITLE_ID).zip"
|
||||
rm -rf "$(abspath $(NATIVE_OUT))/$(TITLE_ID)" "$(abspath $(NATIVE_OUT))/$(RELEASE_ZIP)"
|
||||
cp -r $(APP_OUT) "$(abspath $(NATIVE_OUT))/"
|
||||
cd "$(abspath $(NATIVE_OUT))" && python3 -m zipfile -c $(TITLE_ID).zip $(TITLE_ID)
|
||||
cd "$(abspath $(NATIVE_OUT))" && python3 -m zipfile -c $(RELEASE_ZIP) $(TITLE_ID)
|
||||
cp build/app/mupen64plus-pie.elf "$(abspath $(NATIVE_OUT))/$(TITLE_ID).debug.elf"
|
||||
mkdir -p "$(abspath $(NATIVE_OUT))/release-assets/$(CONTENT_VERSION)"
|
||||
cp "$(abspath $(NATIVE_OUT))/$(TITLE_ID).zip" "$(abspath $(NATIVE_OUT))/release-assets/$(CONTENT_VERSION)/"
|
||||
cp "$(abspath $(NATIVE_OUT))/$(RELEASE_ZIP)" "$(abspath $(NATIVE_OUT))/release-assets/$(CONTENT_VERSION)/"
|
||||
@if [ "$$(echo $(FORMAT) | tr A-Z a-z)" = ffpfsc ]; then \
|
||||
mkpfs=$$(bash $(PS5_NATIVE_BOILERPLATE)/tools/setup-packaging-dependencies.sh ffpfsc) && \
|
||||
rm -f "$(abspath $(NATIVE_OUT))/$(TITLE_ID).ffpfsc" && \
|
||||
"$$mkpfs" pack folder --no-adjust-output-file-extension --version PS5 --verify \
|
||||
"$(abspath $(NATIVE_OUT))/$(TITLE_ID)" "$(abspath $(NATIVE_OUT))/$(TITLE_ID).ffpfsc"; \
|
||||
fi
|
||||
@python3 -m zipfile -l "$(abspath $(NATIVE_OUT))/$(TITLE_ID).zip"
|
||||
@python3 -m zipfile -l "$(abspath $(NATIVE_OUT))/$(RELEASE_ZIP)"
|
||||
@echo "Native app: $(abspath $(NATIVE_OUT))/$(TITLE_ID) (copy this folder to /data/homebrew/), version $(CONTENT_VERSION)"
|
||||
|
||||
# the extra release files: the optional installer payload and the sources
|
||||
|
||||
+18
-7
@@ -15,7 +15,13 @@ its files): no payload to send.
|
||||
The PS5 layer and the build come from **Snes9x PS5** (`../../snes9xPS5-2.0/ps5`), which follows the PS5SX2
|
||||
model.
|
||||
|
||||
> **0.6:** released as the app folder (`PPSA99064.zip`): copy `PPSA99064/` to
|
||||
> **0.6.3:** in-app updates work. Up to 0.6.2 the updater wrote the new files without execute permission, so
|
||||
> the console refused to start the updated app ("Can't start the game or app", CE-107750-0). 0.6.3 unpacks the
|
||||
> update next to the app folder with the permissions an FTP copy gets, closes, and its helper swaps the folders
|
||||
> so ShadowMountPlus installs the app again. The release zip is now `Mupen64PlusPS5.zip`, so 0.6.0–0.6.2 never
|
||||
> offer an update they would break: install 0.6.3 by hand once.
|
||||
>
|
||||
> **0.6:** released as the app folder (`Mupen64PlusPS5.zip`): copy `PPSA99064/` to
|
||||
> `/data/homebrew/` or a USB drive's `homebrew/`, and ShadowMountPlus lists it. No `.elf` to send: eboot.bin
|
||||
> starts the helper it carries through the ELF loader by itself, and now prefers it over etaHEN's daemon.
|
||||
> The app updates itself from the GitHub releases ([Updates](#updates)).
|
||||
@@ -87,7 +93,7 @@ angrylion renders on the CPU, which the PS5's eight Zen 2 cores can afford.
|
||||
|
||||
## Install and play
|
||||
|
||||
1. **Copy the `PPSA99064` folder** from `PPSA99064.zip` (the folder itself, not just its
|
||||
1. **Copy the `PPSA99064` folder** from `Mupen64PlusPS5.zip` (the folder itself, not just its
|
||||
contents) to one of:
|
||||
- `/data/homebrew/PPSA99064` on the console (FTP, PS5Upload...);
|
||||
- `homebrew/PPSA99064` on an exFAT USB drive.
|
||||
@@ -170,12 +176,17 @@ At every start, before it asks for `/data` (the console's HTTPS works only then,
|
||||
`api.github.com` for the latest release of `TheRealRetro/mupen64plus-ps5` (`GITHUB_REPO` in the Makefile).
|
||||
|
||||
- **When the release's tag is newer** than the app's `contentVersion`, it downloads the release's
|
||||
`PPSA99064.zip` and checks its size and SHA-256 against the release (`frontend/fe_update.cpp`).
|
||||
`Mupen64PlusPS5.zip` and checks its size and SHA-256 against the release (`frontend/fe_update.cpp`).
|
||||
- **Once `/data` is visible,** it shows "Mupen64Plus PS5 X is available", with the release notes:
|
||||
- ✕ writes every file over each copy of the app folder it finds (`/data/homebrew/PPSA99064`, a USB drive's
|
||||
`homebrew/PPSA99064`...), each through a temporary `.new` name, and restarts the app;
|
||||
- ✕ unpacks the new version next to each copy of the app folder it finds (`/data/homebrew/PPSA99064`, a USB
|
||||
drive's `homebrew/PPSA99064`...), in `.mupen64plus-update/new/`, with the permissions an FTP copy gets
|
||||
(0777: the console doesn't start an `eboot.bin` without execute permission), writes a job file, starts its
|
||||
helper through the ELF loader and closes. The helper waits for the app to exit, moves the app folder aside,
|
||||
asks ShadowMountPlus to rescan (its API, 127.0.0.1:10101) until it has dropped the app, moves the new
|
||||
version in, rescans until it is installed again, and says so in a notification
|
||||
(`coreorbis/orbis-shims/ProsperoUpdateJob.cpp`; its steps go to `logs/helper.log`);
|
||||
- ○ carries on (asked again next start).
|
||||
- **A release that was installed but didn't take** (the restart still ran an older eboot) isn't offered
|
||||
- **A release that was installed but didn't take** (the next start still ran an older eboot) isn't offered
|
||||
again (`/data/mupen64plus/update/installed.txt`).
|
||||
- **Settings → Check for updates: No** skips the question.
|
||||
- **An app running from a `.ffpfsc` image** can't be updated this way (read-only): copy the new folder by hand.
|
||||
@@ -222,7 +233,7 @@ build-native.bat Ffpfsc also a compressed .ffpfsc image
|
||||
Output in `build-native/` (next to `ps5/`):
|
||||
|
||||
- `PPSA99064/`: the app folder to copy to `/data/homebrew/`.
|
||||
- `PPSA99064.zip`: the release artifact (just the folder). Its tag is the `contentVersion`, made from
|
||||
- `Mupen64PlusPS5.zip`: the release artifact (just the folder). Its tag is the `contentVersion`, made from
|
||||
`VERSION` in the Makefile (0.6.0 -> `00.006.000`); a copy is kept in `release-assets/<contentVersion>/`.
|
||||
- `PPSA99064.debug.elf`: the program with its symbols, for reading crash reports.
|
||||
- `PPSA99064.ffpfsc`: with `Ffpfsc`, a compressed image ShadowMountPlus can mount.
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"conceptId": "99064",
|
||||
"contentBadgeType": 1,
|
||||
"contentId": "UP9000-PPSA99064_00-MUPEN64PS5000001",
|
||||
"contentVersion": "00.006.002",
|
||||
"contentVersion": "00.006.003",
|
||||
"downloadDataSize": 256,
|
||||
"gameIntent": {
|
||||
"permittedIntents": [
|
||||
|
||||
@@ -199,6 +199,11 @@ bool FetchWantedCovers(std::string& text)
|
||||
return r == 1;
|
||||
}
|
||||
|
||||
bool StartHelper()
|
||||
{
|
||||
return SendHelper();
|
||||
}
|
||||
|
||||
bool RequestForSelf(std::string& how)
|
||||
{
|
||||
const int own = PortFromEnv("N64PS5_HELPER_PORT", kHelperPort);
|
||||
|
||||
@@ -48,6 +48,10 @@ constexpr int32_t kRetUntouched = -1337;
|
||||
// Every step goes to OrbisLog (kept in memory until boot.log can be opened).
|
||||
bool RequestForSelf(std::string& how);
|
||||
|
||||
// The app side: sends the helper built into the app to the ELF loader, even when one is running (a new copy
|
||||
// runs a pending update job, then leaves). True when every byte went out.
|
||||
bool StartHelper();
|
||||
|
||||
// The app side, before the jailbreak: the wanted-covers list from the helper (starting the helper through the
|
||||
// ELF loader if none answers). False when no helper could be reached; `text` may be empty (nothing wanted).
|
||||
bool FetchWantedCovers(std::string& text);
|
||||
|
||||
@@ -0,0 +1,368 @@
|
||||
// Mupen64Plus PS5: installing a downloaded update (ProsperoUpdateJob.h).
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "ProsperoUpdateJob.h"
|
||||
|
||||
#include "OrbisPaths.h"
|
||||
#include "ProsperoNotify.h"
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#include <cerrno>
|
||||
#include <csignal>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <ctime>
|
||||
#include <dirent.h>
|
||||
#include <netinet/in.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#ifndef N64PS5_TITLE_ID
|
||||
#define N64PS5_TITLE_ID "PPSA99064"
|
||||
#endif
|
||||
|
||||
namespace updatejob
|
||||
{
|
||||
namespace
|
||||
{
|
||||
constexpr const char* kUpdateDir = ".mupen64plus-update";
|
||||
|
||||
bool Exists(const std::string& path)
|
||||
{
|
||||
struct stat st;
|
||||
return lstat(path.c_str(), &st) == 0;
|
||||
}
|
||||
|
||||
// /user/app/<TITLE>/mount.lnk: ShadowMountPlus's record of where an installed folder app lives
|
||||
// (N64PS5_APP_LINK_DIR replaces /user/app in the host tests).
|
||||
std::string MountLink()
|
||||
{
|
||||
const char* dir = getenv("N64PS5_APP_LINK_DIR");
|
||||
return std::string(dir && *dir ? dir : "/user/app") + "/" N64PS5_TITLE_ID "/mount.lnk";
|
||||
}
|
||||
|
||||
// POST /api/v1/scan to ShadowMountPlus (N64PS5_SMP_PORT replaces 10101 in the host tests). The answer's
|
||||
// status line goes to the log; false when nobody listens.
|
||||
bool AskForScan()
|
||||
{
|
||||
const char* p = getenv("N64PS5_SMP_PORT");
|
||||
const int port = p && *p ? atoi(p) : 10101;
|
||||
const int fd = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (fd < 0)
|
||||
return false;
|
||||
timeval tv = {3, 0};
|
||||
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
|
||||
setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
|
||||
sockaddr_in sa;
|
||||
memset(&sa, 0, sizeof(sa));
|
||||
#ifdef __FreeBSD__
|
||||
sa.sin_len = sizeof(sa);
|
||||
#endif
|
||||
sa.sin_family = AF_INET;
|
||||
sa.sin_port = htons(uint16_t(port));
|
||||
sa.sin_addr.s_addr = htonl(0x7F000001);
|
||||
if (connect(fd, reinterpret_cast<sockaddr*>(&sa), sizeof(sa)) != 0)
|
||||
{
|
||||
close(fd);
|
||||
OrbisLog("[update-job] ShadowMountPlus API (port %d): not there (errno %d)", port, errno);
|
||||
return false;
|
||||
}
|
||||
static const char req[] = "POST /api/v1/scan HTTP/1.0\r\nHost: 127.0.0.1\r\nContent-Type: application/json\r\n"
|
||||
"Content-Length: 2\r\nConnection: close\r\n\r\n{}";
|
||||
bool ok = send(fd, req, sizeof(req) - 1, 0) == ssize_t(sizeof(req) - 1);
|
||||
char buf[256] = {};
|
||||
const ssize_t n = ok ? recv(fd, buf, sizeof(buf) - 1, 0) : -1;
|
||||
close(fd);
|
||||
if (n > 0)
|
||||
{
|
||||
buf[n] = 0;
|
||||
buf[strcspn(buf, "\r\n")] = 0;
|
||||
}
|
||||
OrbisLog("[update-job] ShadowMountPlus rescan: %s", n > 0 ? buf : "no answer");
|
||||
return n > 0;
|
||||
}
|
||||
|
||||
// Waits (asking for rescans every 15 s) until the mount link exists == want. False after `seconds`.
|
||||
bool WaitForLink(bool want, int seconds)
|
||||
{
|
||||
const std::string link = MountLink();
|
||||
for (int i = 0; i <= seconds; i++)
|
||||
{
|
||||
if (Exists(link) == want)
|
||||
{
|
||||
OrbisLog("[update-job] mount link %s after %d s", want ? "back" : "gone", i);
|
||||
return true;
|
||||
}
|
||||
if (i % 15 == 0)
|
||||
AskForScan();
|
||||
sleep(1);
|
||||
}
|
||||
OrbisLog("[update-job] mount link still %s after %d s", want ? "missing" : "there", seconds);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ProcessAlive(int pid)
|
||||
{
|
||||
return pid > 0 && (kill(pid, 0) == 0 || errno != ESRCH);
|
||||
}
|
||||
|
||||
bool RemoveTreeRaw(const std::string& path)
|
||||
{
|
||||
struct stat st;
|
||||
if (lstat(path.c_str(), &st) != 0)
|
||||
return errno == ENOENT;
|
||||
if (!S_ISDIR(st.st_mode))
|
||||
return unlink(path.c_str()) == 0;
|
||||
bool ok = true;
|
||||
if (DIR* d = opendir(path.c_str()))
|
||||
{
|
||||
while (dirent* e = readdir(d))
|
||||
{
|
||||
if (strcmp(e->d_name, ".") == 0 || strcmp(e->d_name, "..") == 0)
|
||||
continue;
|
||||
ok = RemoveTreeRaw(path + "/" + e->d_name) && ok;
|
||||
}
|
||||
closedir(d);
|
||||
}
|
||||
return rmdir(path.c_str()) == 0 && ok;
|
||||
}
|
||||
|
||||
bool Read(Job& job)
|
||||
{
|
||||
FILE* f = fopen(JobPath().c_str(), "r");
|
||||
if (!f)
|
||||
return false;
|
||||
char line[1024];
|
||||
while (fgets(line, sizeof(line), f))
|
||||
{
|
||||
line[strcspn(line, "\r\n")] = 0;
|
||||
const std::string s = line;
|
||||
if (s.rfind("pid=", 0) == 0)
|
||||
job.pid = atoi(s.c_str() + 4);
|
||||
else if (s.rfind("tag=", 0) == 0)
|
||||
job.tag = s.substr(4);
|
||||
else if (s.rfind("version=", 0) == 0)
|
||||
job.version = s.substr(8);
|
||||
else if (s.rfind("target=", 0) == 0)
|
||||
{
|
||||
const std::string v = s.substr(7);
|
||||
const size_t a = v.find('|'), b = v.find('|', a == std::string::npos ? a : a + 1);
|
||||
if (a == std::string::npos || b == std::string::npos)
|
||||
continue;
|
||||
Target t;
|
||||
t.dir = v.substr(0, a);
|
||||
t.staging = v.substr(a + 1, b - a - 1);
|
||||
t.old = v.substr(b + 1);
|
||||
job.targets.push_back(t);
|
||||
}
|
||||
}
|
||||
fclose(f);
|
||||
return !job.targets.empty();
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Target TargetFor(const std::string& app_dir)
|
||||
{
|
||||
Target t;
|
||||
t.dir = app_dir;
|
||||
const size_t slash = app_dir.rfind('/');
|
||||
const std::string parent = slash == std::string::npos ? "." : app_dir.substr(0, slash);
|
||||
const std::string name = slash == std::string::npos ? app_dir : app_dir.substr(slash + 1);
|
||||
t.staging = parent + "/" + kUpdateDir + "/new/" + name;
|
||||
t.old = parent + "/" + kUpdateDir + "/old/" + name;
|
||||
return t;
|
||||
}
|
||||
|
||||
std::string JobPath()
|
||||
{
|
||||
return OrbisDir("update") + "/job.txt";
|
||||
}
|
||||
|
||||
bool Write(const Job& job, std::string& why)
|
||||
{
|
||||
OrbisMkdirs(OrbisDir("update"));
|
||||
const std::string path = JobPath(), tmp = path + ".part";
|
||||
FILE* f = fopen(tmp.c_str(), "w");
|
||||
if (!f)
|
||||
{
|
||||
why = "can't write " + tmp;
|
||||
return false;
|
||||
}
|
||||
fprintf(f, "pid=%d\ntag=%s\nversion=%s\n", job.pid, job.tag.c_str(), job.version.c_str());
|
||||
for (const Target& t : job.targets)
|
||||
fprintf(f, "target=%s|%s|%s\n", t.dir.c_str(), t.staging.c_str(), t.old.c_str());
|
||||
const bool ok = fclose(f) == 0;
|
||||
if (!ok || rename(tmp.c_str(), path.c_str()) != 0)
|
||||
{
|
||||
unlink(tmp.c_str());
|
||||
why = "can't write " + path;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
int MakeRunnable(const std::string& tree)
|
||||
{
|
||||
struct stat st;
|
||||
if (lstat(tree.c_str(), &st) != 0 || S_ISLNK(st.st_mode))
|
||||
return 0;
|
||||
int changed = 0;
|
||||
if ((st.st_mode & 07777) != 0777)
|
||||
changed += chmod(tree.c_str(), 0777) == 0;
|
||||
if (S_ISDIR(st.st_mode))
|
||||
if (DIR* d = opendir(tree.c_str()))
|
||||
{
|
||||
while (dirent* e = readdir(d))
|
||||
if (strcmp(e->d_name, ".") != 0 && strcmp(e->d_name, "..") != 0)
|
||||
changed += MakeRunnable(tree + "/" + e->d_name);
|
||||
closedir(d);
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
std::string EbootMode(const std::string& app_dir)
|
||||
{
|
||||
struct stat st;
|
||||
if (stat((app_dir + "/eboot.bin").c_str(), &st) != 0)
|
||||
return "missing";
|
||||
char buf[16];
|
||||
snprintf(buf, sizeof(buf), "%04o", unsigned(st.st_mode & 07777));
|
||||
return buf;
|
||||
}
|
||||
|
||||
bool RemoveUpdateTree(const std::string& path)
|
||||
{
|
||||
if (path.find(std::string("/") + kUpdateDir + "/") == std::string::npos)
|
||||
return false; // never anything else
|
||||
return RemoveTreeRaw(path);
|
||||
}
|
||||
|
||||
void RunIfPending()
|
||||
{
|
||||
Job job;
|
||||
if (!Read(job))
|
||||
return;
|
||||
// take the job first: a second helper started meanwhile must not run it too
|
||||
const std::string taken = JobPath() + ".running";
|
||||
if (rename(JobPath().c_str(), taken.c_str()) != 0)
|
||||
return;
|
||||
OrbisLog("[update-job] installing %s (%s) for pid %d, %zu folder(s)", job.version.c_str(), job.tag.c_str(),
|
||||
job.pid, job.targets.size());
|
||||
|
||||
// 1. the app closes itself right after starting this payload
|
||||
int waited = 0;
|
||||
while (ProcessAlive(job.pid) && waited < 60)
|
||||
{
|
||||
sleep(1);
|
||||
waited++;
|
||||
}
|
||||
if (ProcessAlive(job.pid))
|
||||
{
|
||||
OrbisLog("[update-job] pid %d still running after 60 s: giving up", job.pid);
|
||||
ProsperoNotify("Mupen64Plus PS5: the update to %s was not installed (the app didn't close). Open the app to "
|
||||
"try again.",
|
||||
job.version.c_str());
|
||||
unlink(taken.c_str());
|
||||
return;
|
||||
}
|
||||
sleep(2); // ShadowMountPlus releases the app's runtime mounts after it exits
|
||||
ProsperoNotify("Mupen64Plus PS5: installing version %s...", job.version.c_str());
|
||||
|
||||
const bool tracked = Exists(MountLink());
|
||||
OrbisLog("[update-job] ShadowMountPlus mount link %s: %s", MountLink().c_str(),
|
||||
tracked ? "present" : "absent (folders are swapped without waiting for it)");
|
||||
|
||||
// 2. the app folders aside
|
||||
std::vector<Target> moved;
|
||||
std::string why;
|
||||
for (const Target& t : job.targets)
|
||||
{
|
||||
if (!Exists(t.staging))
|
||||
{
|
||||
why = "the new version is missing (" + t.staging + ")";
|
||||
break;
|
||||
}
|
||||
const std::string before = EbootMode(t.staging);
|
||||
const int changed = MakeRunnable(t.staging);
|
||||
OrbisLog("[update-job] %s: eboot.bin %s, installed copy %s; new version's permissions set (%d changed): "
|
||||
"eboot.bin now %s",
|
||||
t.dir.c_str(), before.c_str(), EbootMode(t.dir).c_str(), changed, EbootMode(t.staging).c_str());
|
||||
RemoveUpdateTree(t.old);
|
||||
OrbisMkdirs(t.old.substr(0, t.old.rfind('/')));
|
||||
if (rename(t.dir.c_str(), t.old.c_str()) != 0)
|
||||
{
|
||||
why = "can't move " + t.dir + " aside (errno " + std::to_string(errno) + ")";
|
||||
break;
|
||||
}
|
||||
moved.push_back(t);
|
||||
}
|
||||
auto roll_back = [&]() {
|
||||
for (const Target& t : moved)
|
||||
{
|
||||
if (!Exists(t.dir))
|
||||
rename(t.old.c_str(), t.dir.c_str());
|
||||
}
|
||||
};
|
||||
if (!why.empty())
|
||||
{
|
||||
OrbisLog("[update-job] %s: putting everything back", why.c_str());
|
||||
roll_back();
|
||||
if (tracked)
|
||||
AskForScan();
|
||||
ProsperoNotify("Mupen64Plus PS5: the update to %s failed (%s).", job.version.c_str(), why.c_str());
|
||||
unlink(taken.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. ShadowMountPlus forgets the old installation
|
||||
if (tracked)
|
||||
WaitForLink(false, 90);
|
||||
|
||||
// 4. the new version in place, installed again
|
||||
for (const Target& t : moved)
|
||||
{
|
||||
if (rename(t.staging.c_str(), t.dir.c_str()) != 0)
|
||||
{
|
||||
why = "can't move the new version into " + t.dir + " (errno " + std::to_string(errno) + ")";
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!why.empty())
|
||||
{
|
||||
OrbisLog("[update-job] %s: putting the old version back", why.c_str());
|
||||
for (const Target& t : moved)
|
||||
if (Exists(t.dir) && !Exists(t.staging))
|
||||
rename(t.dir.c_str(), t.staging.c_str()); // the new copy back out of the way
|
||||
roll_back();
|
||||
if (tracked)
|
||||
WaitForLink(true, 120);
|
||||
ProsperoNotify("Mupen64Plus PS5: the update to %s failed (%s).", job.version.c_str(), why.c_str());
|
||||
unlink(taken.c_str());
|
||||
return;
|
||||
}
|
||||
const bool installed = !tracked || WaitForLink(true, 120);
|
||||
|
||||
// 5. tidy up
|
||||
for (const Target& t : moved)
|
||||
{
|
||||
RemoveUpdateTree(t.old);
|
||||
const std::string root = t.old.substr(0, t.old.rfind("/old/"));
|
||||
rmdir((root + "/old").c_str());
|
||||
rmdir((root + "/new").c_str());
|
||||
rmdir(root.c_str());
|
||||
}
|
||||
unlink(taken.c_str());
|
||||
OrbisLog("[update-job] version %s installed%s", job.version.c_str(),
|
||||
installed ? "" : " (ShadowMountPlus hasn't confirmed it yet)");
|
||||
if (installed)
|
||||
ProsperoNotify("Mupen64Plus PS5 updated to %s. Open it from the home screen.", job.version.c_str());
|
||||
else
|
||||
ProsperoNotify("Mupen64Plus PS5 updated to %s. Wait a minute before opening it (or restart the PS5 if it "
|
||||
"doesn't start).",
|
||||
job.version.c_str());
|
||||
}
|
||||
} // namespace updatejob
|
||||
@@ -0,0 +1,60 @@
|
||||
// Mupen64Plus PS5: installing a downloaded update (frontend/fe_update.h) so the console starts it.
|
||||
//
|
||||
// Replacing the files of an installed app in place leaves it unable to start ("Can't start the game or app",
|
||||
// confirmed on the console with 0.6.0 -> 0.6.1 and 0.6.1 -> 0.6.2, whether the app or an FTP client wrote
|
||||
// them): ShadowMountPlus keeps the installation it made from the old files. A fresh copy works because
|
||||
// ShadowMountPlus sees the folder disappear ("source removed", its mount link /user/app/<TITLE>/mount.lnk goes)
|
||||
// and installs it again when it comes back ("Installed NEW!"). The update does the same:
|
||||
// 1. the app unpacks the new version into a staging folder next to the app folder
|
||||
// (<parent>/.mupen64plus-update/new/PPSA99064, too deep for ShadowMountPlus's scan), writes a job file,
|
||||
// starts its helper payload through the ELF loader, and closes;
|
||||
// 2. the helper (RunIfPending) waits for the app to exit, moves the app folder aside (.../old/PPSA99064),
|
||||
// asks ShadowMountPlus to rescan (its HTTP API, 127.0.0.1:10101) until the mount link is gone, moves the
|
||||
// new folder in, rescans until the link is back (installed again), removes the old copy and says so in
|
||||
// a notification. Anything failing puts the old folder back.
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace updatejob
|
||||
{
|
||||
struct Target
|
||||
{
|
||||
std::string dir; // the app folder, e.g. /data/homebrew/PPSA99064
|
||||
std::string staging; // the new version, unpacked
|
||||
std::string old; // where the app folder goes while ShadowMountPlus forgets it
|
||||
};
|
||||
|
||||
struct Job
|
||||
{
|
||||
int pid = 0; // the app, which must have exited before the folders move
|
||||
std::string tag; // 00.006.003
|
||||
std::string version; // 0.6.3
|
||||
std::vector<Target> targets;
|
||||
};
|
||||
|
||||
// The staging and old paths for an app folder.
|
||||
Target TargetFor(const std::string& app_dir);
|
||||
|
||||
// /data/mupen64plus/update/job.txt
|
||||
std::string JobPath();
|
||||
bool Write(const Job& job, std::string& why);
|
||||
|
||||
// Deletes a folder tree, only inside a ".mupen64plus-update" folder (staging and old copies).
|
||||
bool RemoveUpdateTree(const std::string& path);
|
||||
|
||||
// Gives every file and folder in a tree the permissions an FTP copy gets (0777). Files a program writes with
|
||||
// fopen() have no execute permission (0666 at most), and the console doesn't start an eboot.bin without it:
|
||||
// that, not the files' contents, is why every in-app update so far left the app unable to start
|
||||
// ("Can't start the game or app", CE-107750-0). The number of entries changed.
|
||||
int MakeRunnable(const std::string& tree);
|
||||
|
||||
// eboot.bin's permissions in a folder, for the logs ("0777", or "missing").
|
||||
std::string EbootMode(const std::string& app_dir);
|
||||
|
||||
// The helper payload, at start: performs a pending job (and deletes it). Returns at once when there is none.
|
||||
void RunIfPending();
|
||||
} // namespace updatejob
|
||||
+48
-36
@@ -8,7 +8,9 @@
|
||||
#include "fe_menu.h"
|
||||
|
||||
#include "OrbisPaths.h"
|
||||
#include "ProsperoJailbreak.h"
|
||||
#include "ProsperoNotify.h"
|
||||
#include "ProsperoUpdateJob.h"
|
||||
#include "ProsperoSce.h"
|
||||
|
||||
#include "unzip.h"
|
||||
@@ -32,6 +34,9 @@
|
||||
#ifndef N64PS5_TITLE_ID
|
||||
#define N64PS5_TITLE_ID "PPSA99064"
|
||||
#endif
|
||||
#ifndef N64PS5_RELEASE_ZIP
|
||||
#define N64PS5_RELEASE_ZIP "Mupen64PlusPS5.zip"
|
||||
#endif
|
||||
#ifndef N64PS5_GITHUB_REPO
|
||||
#define N64PS5_GITHUB_REPO "TheRealRetro/mupen64plus-ps5"
|
||||
#endif
|
||||
@@ -43,7 +48,7 @@ namespace
|
||||
constexpr size_t kMaxReleaseJson = 1u << 20;
|
||||
constexpr size_t kMaxZip = 64u << 20;
|
||||
constexpr size_t kMaxFile = 32u << 20;
|
||||
const char* const kAsset = N64PS5_TITLE_ID ".zip";
|
||||
const char* const kAsset = N64PS5_RELEASE_ZIP; // not PPSA99064.zip: see RELEASE_ZIP in the Makefile
|
||||
|
||||
// ---- a small JSON reader (the release object) --------------------------------------------------------------
|
||||
struct Json
|
||||
@@ -564,32 +569,24 @@ bool WriteFile(const std::string& path, const std::vector<uint8_t>& data)
|
||||
return fclose(f) == 0 && ok;
|
||||
}
|
||||
|
||||
// The new files over one app folder: all written as "<file>.new" first, then renamed into place (the
|
||||
// running eboot.bin keeps its old copy open until the restart).
|
||||
bool InstallInto(const std::string& dir, const std::map<std::string, std::vector<uint8_t>>& files, std::string& why)
|
||||
// The new version, complete, in the staging folder next to an app folder (ProsperoUpdateJob.h): the installed
|
||||
// files themselves are never written over (the console then can't start the app: 0.6.1 and 0.6.2).
|
||||
bool StageInto(const updatejob::Target& t, const std::map<std::string, std::vector<uint8_t>>& files, std::string& why)
|
||||
{
|
||||
updatejob::RemoveUpdateTree(t.staging);
|
||||
for (const auto& f : files)
|
||||
{
|
||||
const std::string path = dir + "/" + f.first;
|
||||
const size_t slash = path.rfind('/');
|
||||
OrbisMkdirs(path.substr(0, slash));
|
||||
if (!WriteFile(path + ".new", f.second))
|
||||
const std::string path = t.staging + "/" + f.first;
|
||||
OrbisMkdirs(path.substr(0, path.rfind('/')));
|
||||
if (!WriteFile(path, f.second))
|
||||
{
|
||||
why = "can't write " + path + ".new";
|
||||
for (const auto& g : files)
|
||||
unlink((dir + "/" + g.first + ".new").c_str());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (const auto& f : files)
|
||||
{
|
||||
const std::string path = dir + "/" + f.first;
|
||||
if (rename((path + ".new").c_str(), path.c_str()) != 0)
|
||||
{
|
||||
why = "can't replace " + path;
|
||||
why = "can't write " + path;
|
||||
updatejob::RemoveUpdateTree(t.staging);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// fopen() gives no execute permission, and the console doesn't start an eboot.bin without it
|
||||
updatejob::MakeRunnable(t.staging);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -598,21 +595,19 @@ std::string StampPath()
|
||||
return OrbisDir("update") + "/installed.txt";
|
||||
}
|
||||
|
||||
void Restart(const UpdateOffer& offer)
|
||||
// The helper installs the staged version once the app has closed (ProsperoUpdateJob.h): say so, then close
|
||||
// the way main-boot.cpp's ExitApp does.
|
||||
[[noreturn]] void CloseForUpdate(const UpdateOffer& offer)
|
||||
{
|
||||
const char* path = "/data/homebrew/" N64PS5_TITLE_ID "/eboot.bin";
|
||||
if (access(path, F_OK) != 0)
|
||||
path = "/app0/eboot.bin";
|
||||
OrbisLog("[update] installed %s: restarting %s", offer.tag.c_str(), path);
|
||||
MessageBox("Installing Mupen64Plus PS5 " + offer.version,
|
||||
"The app closes now. Open it again from the home screen when the notification says the update is installed.");
|
||||
OrbisLog("[update] closing so the helper can install %s", offer.tag.c_str());
|
||||
ProsperoNotifyFlush();
|
||||
OrbisLogClose();
|
||||
const int rc = sceSystemServiceLoadExec(path, nullptr);
|
||||
if (rc == 0)
|
||||
if (sceSystemServiceLoadExec("exit", nullptr) == 0)
|
||||
for (int i = 0; i < 100; i++)
|
||||
usleep(100 * 1000);
|
||||
OrbisLogOpen("boot-after-update");
|
||||
OrbisLog("[update] sceSystemServiceLoadExec(%s) -> %x", path, unsigned(rc));
|
||||
MessageBox("Mupen64Plus PS5 " + offer.version + " is installed", "Close the app and open it again to use it.");
|
||||
_exit(0);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
@@ -760,14 +755,18 @@ void OfferUpdate(UpdateOffer& offer, bool ask)
|
||||
"copy the new folder by hand)";
|
||||
}
|
||||
}
|
||||
int done = 0;
|
||||
updatejob::Job job;
|
||||
job.pid = int(getpid());
|
||||
job.tag = offer.tag;
|
||||
job.version = offer.version;
|
||||
for (const std::string& dir : folders)
|
||||
{
|
||||
const updatejob::Target t = updatejob::TargetFor(dir);
|
||||
std::string err;
|
||||
if (InstallInto(dir, files, err))
|
||||
if (StageInto(t, files, err))
|
||||
{
|
||||
done++;
|
||||
OrbisLog("[update] %s: %zu files written", dir.c_str(), files.size());
|
||||
job.targets.push_back(t);
|
||||
OrbisLog("[update] %s: %zu files staged in %s", dir.c_str(), files.size(), t.staging.c_str());
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -775,9 +774,22 @@ void OfferUpdate(UpdateOffer& offer, bool ask)
|
||||
why = err;
|
||||
}
|
||||
}
|
||||
if (!ok || done == 0)
|
||||
if (ok && job.targets.empty())
|
||||
ok = false;
|
||||
if (ok && !updatejob::Write(job, why))
|
||||
ok = false;
|
||||
// the helper that installs it: a fresh copy through the ELF loader (one already running only lets apps out)
|
||||
if (ok && !jailbreak::StartHelper())
|
||||
{
|
||||
ok = false;
|
||||
why = "the ELF loader (port 9021) didn't answer, so the update can't be installed now";
|
||||
}
|
||||
if (!ok)
|
||||
{
|
||||
OrbisLog("[update] %s failed: %s", offer.tag.c_str(), why.c_str());
|
||||
unlink(updatejob::JobPath().c_str());
|
||||
for (const updatejob::Target& t : job.targets)
|
||||
updatejob::RemoveUpdateTree(t.staging);
|
||||
MessageBox("The update failed", why);
|
||||
return;
|
||||
}
|
||||
@@ -787,6 +799,6 @@ void OfferUpdate(UpdateOffer& offer, bool ask)
|
||||
fclose(f);
|
||||
}
|
||||
offer.zip.clear();
|
||||
Restart(offer);
|
||||
CloseForUpdate(offer);
|
||||
}
|
||||
} // namespace fe
|
||||
@@ -1,15 +1,16 @@
|
||||
// Mupen64Plus PS5: updates from the project's GitHub releases (github.com/TheRealRetro/mupen64plus-ps5).
|
||||
//
|
||||
// A release is tagged with param.json's contentVersion (NN.NNN.NNN, 0.6.0 -> 00.006.000) and carries
|
||||
// PPSA99064.zip (the PPSA99064/ app folder), as the Homebrew Browser's releases do; GitHub publishes each
|
||||
// asset's SHA-256 ("digest") with the release.
|
||||
// Mupen64PlusPS5.zip (the PPSA99064/ app folder; RELEASE_ZIP in the Makefile, PPSA99064.zip up to 0.6.2);
|
||||
// GitHub publishes each asset's SHA-256 ("digest") with the release.
|
||||
//
|
||||
// Two steps, because of where HTTPS works (fe_prefetch.h: before the app asks for /data, not after):
|
||||
// 1. CheckForUpdate, at start-up before the jailbreak: asks api.github.com for the latest release; when its
|
||||
// tag is newer than this build, downloads PPSA99064.zip and checks its size and SHA-256 (in memory).
|
||||
// tag is newer than this build, downloads Mupen64PlusPS5.zip and checks its size and SHA-256 (in memory).
|
||||
// 2. OfferUpdate, once /data is visible: "Version X is available" with the release notes; on Cross it
|
||||
// writes the new files over every copy of the app folder it finds (/data/homebrew/PPSA99064, the USB
|
||||
// drives' homebrew/PPSA99064...), each file through a temporary name, and starts the app again.
|
||||
// unpacks the new version next to every copy of the app folder it finds (/data/homebrew/PPSA99064, the
|
||||
// USB drives' homebrew/PPSA99064...) and closes; its helper payload then swaps the folders so
|
||||
// ShadowMountPlus installs the app again (ProsperoUpdateJob.h), and a notification says when to reopen it.
|
||||
// The setting "Check for updates" (Off) skips the question; the download in step 1 still happens, since the
|
||||
// settings can't be read before the jailbreak.
|
||||
//
|
||||
@@ -34,8 +35,8 @@ struct UpdateOffer
|
||||
// Step 1 (before the jailbreak). Never blocks for long when the console is offline.
|
||||
UpdateOffer CheckForUpdate();
|
||||
|
||||
// Step 2 (with /data). Returns when there is nothing to do, the user said later, or the install failed;
|
||||
// after a successful install it restarts the app and doesn't return (unless the restart fails).
|
||||
// Step 2 (with /data). Returns when there is nothing to do, the user said later, or staging failed; once the
|
||||
// update is staged it closes the app and doesn't return.
|
||||
void OfferUpdate(UpdateOffer& offer, bool ask);
|
||||
|
||||
// "00.006.000" -> {0, 6, 0}; also accepts "v00.006.000" and "0.6.0". False when it isn't a version.
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include "OrbisPaths.h"
|
||||
#include "ProsperoJailbreak.h"
|
||||
#include "ProsperoNotify.h"
|
||||
#include "ProsperoUpdateJob.h"
|
||||
#ifndef N64PS5_HELPER_ONLY
|
||||
#include "ProsperoInstall.h"
|
||||
#endif
|
||||
@@ -85,6 +86,9 @@ int main()
|
||||
g_ready_message = msg;
|
||||
#endif
|
||||
|
||||
// an update the app staged before closing (ProsperoUpdateJob.h)
|
||||
updatejob::RunIfPending();
|
||||
|
||||
// the helper: runs until the console is turned off, or returns at once when one already runs
|
||||
if (!jailbreak::ServeHelper(OnReady))
|
||||
{
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
# (release.bat): 0.6.1 was published from an incremental build that didn't start on the console, while a clean
|
||||
# build of the same source did.
|
||||
#
|
||||
# Output in build-native/ (next to ps5/): PPSA99064/, PPSA99064.zip, PPSA99064.debug.elf, and PPSA99064.ffpfsc
|
||||
# Output in build-native/ (next to ps5/): PPSA99064/, Mupen64PlusPS5.zip, PPSA99064.debug.elf, and PPSA99064.ffpfsc
|
||||
# with Ffpfsc. The version (param.json's contentVersion) comes from VERSION in ps5/Makefile.
|
||||
#
|
||||
# Environment:
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# release.ps1 - publishes a Mupen64Plus PS5 release on GitHub (run from Windows: release.bat).
|
||||
#
|
||||
# 1. builds the app from scratch (build-native.bat Clean): build-native\PPSA99064\ and PPSA99064.zip. Always
|
||||
# 1. builds the app from scratch (build-native.bat Clean): build-native\PPSA99064\ and Mupen64PlusPS5.zip. Always
|
||||
# clean: 0.6.1 was published from an incremental build that didn't start on the console;
|
||||
# 2. checks that everything is committed and pushed, that release-notes\<contentVersion>.md exists and that
|
||||
# the tag isn't taken;
|
||||
# 3. creates the GitHub release: tag = contentVersion (NN.NNN.NNN, from VERSION in ps5\Makefile), asset
|
||||
# PPSA99064.zip, notes from release-notes\<contentVersion>.md (the app shows them when it offers the
|
||||
# Mupen64PlusPS5.zip, notes from release-notes\<contentVersion>.md (the app shows them when it offers the
|
||||
# update);
|
||||
# 4. reads the release back and compares GitHub's SHA-256 of the asset with the local zip (the app checks
|
||||
# the download against that digest).
|
||||
@@ -17,6 +17,7 @@ $ErrorActionPreference = 'Stop'
|
||||
$root = Resolve-Path (Join-Path $PSScriptRoot '..\..')
|
||||
$repo = 'TheRealRetro/mupen64plus-ps5'
|
||||
$title = 'PPSA99064'
|
||||
$asset = 'Mupen64PlusPS5.zip' # RELEASE_ZIP in ps5\Makefile; PPSA99064.zip up to 0.6.2
|
||||
$gh = (Get-Command gh -ErrorAction SilentlyContinue).Source
|
||||
if (-not $gh) { $gh = 'C:\Program Files\GitHub CLI\gh.exe' }
|
||||
if (-not (Test-Path $gh)) { throw 'GitHub CLI not found: winget install GitHub.cli, then gh auth login' }
|
||||
@@ -25,7 +26,7 @@ Push-Location $root
|
||||
try {
|
||||
& cmd /c "`"$root\build-native.bat`" Clean"
|
||||
if ($LASTEXITCODE -ne 0) { throw 'build failed' }
|
||||
$zip = Join-Path $root "build-native\$title.zip"
|
||||
$zip = Join-Path $root "build-native\$asset"
|
||||
$param = Get-Content (Join-Path $root "build-native\$title\sce_sys\param.json") -Raw | ConvertFrom-Json
|
||||
$cv = $param.contentVersion
|
||||
if ($cv -notmatch '^\d{2}\.\d{3}\.\d{3}$') { throw "contentVersion '$cv' is not NN.NNN.NNN" }
|
||||
@@ -55,7 +56,7 @@ try {
|
||||
$local = (Get-FileHash $zip -Algorithm SHA256).Hash.ToLower()
|
||||
# parsed here: PowerShell 5.1 strips the quotes a --jq filter would need
|
||||
$release = (& $gh api "repos/$repo/releases/tags/$cv") -join "`n" | ConvertFrom-Json
|
||||
$remote = ($release.assets | Where-Object { $_.name -eq "$title.zip" }).digest
|
||||
$remote = ($release.assets | Where-Object { $_.name -eq $asset }).digest
|
||||
if ($remote -ne "sha256:$local") { throw "GitHub's digest '$remote' doesn't match the local zip (sha256:$local)" }
|
||||
Write-Host "Done: https://github.com/$repo/releases/tag/$cv (sha256 $local)"
|
||||
Write-Host 'Consoles running an older version will offer it the next time Mupen64Plus PS5 starts.'
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
## Fixes
|
||||
- In-app updates now install correctly. Before, the updated app wouldn't start ("Can't start the game or app", CE-107750-0): the updater wrote the new files without execute permission.
|
||||
- The update is unpacked next to the app, the app closes, and a background helper installs it. A notification says when to open the app again.
|
||||
|
||||
## If you have 0.6.0, 0.6.1 or 0.6.2
|
||||
- Install this version by hand once: delete /data/homebrew/PPSA99064, then copy the PPSA99064 folder from Mupen64PlusPS5.zip to /data/homebrew/.
|
||||
- Those versions don't offer this update themselves (their updater would break the app). Updates after this one install from inside the app.
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
@echo off
|
||||
rem Publishes a Mupen64Plus PS5 release on GitHub (TheRealRetro/mupen64plus-ps5).
|
||||
rem release.bat clean build, then publish build-native\PPSA99064.zip as release <contentVersion>
|
||||
rem release.bat clean build, then publish build-native\Mupen64PlusPS5.zip as release <contentVersion>
|
||||
rem Before: raise VERSION in ps5\Makefile, write release-notes\<contentVersion>.md, commit and push.
|
||||
rem See ps5\tools\release.ps1.
|
||||
setlocal
|
||||
|
||||
Reference in new issue
Block a user