mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-20 12:40:21 +02:00
Fix Switch OTA launcher self-update via a bootstrap NRO.
A running launcher cannot replace its own NRO on sdmc/FAT; stage the new binary and chainload a tiny helper from romfs to swap it in before loading the game.
This commit is contained in:
@@ -46,6 +46,12 @@ native/switch-ota-launcher/*.elf
|
|||||||
native/switch-ota-launcher/*.map
|
native/switch-ota-launcher/*.map
|
||||||
native/switch-ota-launcher/romfs/logo.png
|
native/switch-ota-launcher/romfs/logo.png
|
||||||
native/switch-ota-launcher/romfs/cacert.pem
|
native/switch-ota-launcher/romfs/cacert.pem
|
||||||
|
native/switch-ota-launcher/romfs/ota-bootstrap.nro
|
||||||
|
native/switch-ota-bootstrap/build/
|
||||||
|
native/switch-ota-bootstrap/*.nro
|
||||||
|
native/switch-ota-bootstrap/*.nacp
|
||||||
|
native/switch-ota-bootstrap/*.elf
|
||||||
|
native/switch-ota-bootstrap/*.map
|
||||||
|
|
||||||
# Legacy manual convenience-copy location (superseded by /dist/android/)
|
# Legacy manual convenience-copy location (superseded by /dist/android/)
|
||||||
mobile/dist/
|
mobile/dist/
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
#---------------------------------------------------------------------------------
|
||||||
|
# One-shot OTA bootstrap NRO (copied into the main launcher's romfs).
|
||||||
|
#---------------------------------------------------------------------------------
|
||||||
|
.SUFFIXES:
|
||||||
|
|
||||||
|
ifeq ($(strip $(DEVKITPRO)),)
|
||||||
|
|
||||||
|
.PHONY: all
|
||||||
|
all:
|
||||||
|
@echo "DEVKITPRO not set — bootstrap builds with the launcher only."
|
||||||
|
@false
|
||||||
|
|
||||||
|
else
|
||||||
|
|
||||||
|
TOPDIR ?= $(CURDIR)
|
||||||
|
include $(DEVKITPRO)/libnx/switch_rules
|
||||||
|
|
||||||
|
TARGET := ota-bootstrap
|
||||||
|
BUILD := build
|
||||||
|
SOURCES := .
|
||||||
|
INCLUDES := include
|
||||||
|
ROMFS :=
|
||||||
|
|
||||||
|
APP_TITLE := gen1recomp OTA
|
||||||
|
APP_AUTHOR := bryanthaboi, port by andrewqsantos
|
||||||
|
APP_VERSION := 0.0.0
|
||||||
|
|
||||||
|
ARCH := -march=armv8-a+crc+crypto -mtune=cortex-a57 -mtp=soft -fPIE
|
||||||
|
CFLAGS := -g -Wall -O2 -ffunction-sections $(ARCH) $(DEFINES)
|
||||||
|
CFLAGS += $(INCLUDE) -D__SWITCH__
|
||||||
|
ASFLAGS := -g $(ARCH)
|
||||||
|
LDFLAGS = -specs=$(DEVKITPRO)/libnx/switch.specs -g $(ARCH) -Wl,-Map,$(notdir $*.map)
|
||||||
|
LIBS := -lnx
|
||||||
|
LIBDIRS := $(LIBNX)
|
||||||
|
|
||||||
|
ifneq ($(BUILD),$(notdir $(CURDIR)))
|
||||||
|
export OUTPUT := $(CURDIR)/$(TARGET)
|
||||||
|
export TOPDIR := $(CURDIR)
|
||||||
|
export VPATH := $(foreach dir,$(SOURCES),$(CURDIR)/$(dir))
|
||||||
|
export DEPSDIR := $(CURDIR)/$(BUILD)
|
||||||
|
CFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.c)))
|
||||||
|
export LD := $(CC)
|
||||||
|
export OFILES := $(CFILES:.c=.o)
|
||||||
|
export INCLUDE := $(foreach dir,$(INCLUDES),-I$(CURDIR)/$(dir)) \
|
||||||
|
$(foreach dir,$(LIBDIRS),-I$(dir)/include) \
|
||||||
|
-I$(CURDIR)/$(BUILD)
|
||||||
|
export LIBPATHS := $(foreach dir,$(LIBDIRS),-L$(dir)/lib)
|
||||||
|
export APP_ICON := $(LIBNX)/default_icon.jpg
|
||||||
|
export NROFLAGS += --icon=$(APP_ICON)
|
||||||
|
export NROFLAGS += --nacp=$(CURDIR)/$(TARGET).nacp
|
||||||
|
|
||||||
|
.PHONY: $(BUILD) clean all
|
||||||
|
|
||||||
|
all: $(BUILD)
|
||||||
|
|
||||||
|
$(BUILD):
|
||||||
|
@[ -d $@ ] || mkdir -p $@
|
||||||
|
@$(MAKE) --no-print-directory -C $(BUILD) -f $(CURDIR)/Makefile
|
||||||
|
|
||||||
|
clean:
|
||||||
|
@echo clean ...
|
||||||
|
@rm -fr $(BUILD) $(TARGET).nro $(TARGET).nacp $(TARGET).elf
|
||||||
|
|
||||||
|
else
|
||||||
|
.PHONY: all
|
||||||
|
DEPENDS := $(OFILES:.o=.d)
|
||||||
|
all: $(OUTPUT).nro
|
||||||
|
$(OUTPUT).nro: $(OUTPUT).elf $(OUTPUT).nacp
|
||||||
|
$(OUTPUT).elf: $(OFILES)
|
||||||
|
-include $(DEPENDS)
|
||||||
|
endif
|
||||||
|
endif
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
* Tiny one-shot helper: swap a staged launcher NRO into place, then load the game.
|
||||||
|
* The main OTA launcher chainloads here because a running NRO cannot replace itself
|
||||||
|
* on sdmc/FAT.
|
||||||
|
*/
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#if defined(__SWITCH__)
|
||||||
|
#include <switch.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define INSTALL_DIR "sdmc:/switch/gen1recomp"
|
||||||
|
#define STAGED INSTALL_DIR "/gen1recomp.nro.staged"
|
||||||
|
#define LAUNCHER INSTALL_DIR "/gen1recomp.nro"
|
||||||
|
#define GAME INSTALL_DIR "/gen1recomp-game.nro"
|
||||||
|
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
(void)argc;
|
||||||
|
(void)argv;
|
||||||
|
|
||||||
|
#if defined(__SWITCH__)
|
||||||
|
remove(LAUNCHER);
|
||||||
|
if (rename(STAGED, LAUNCHER) != 0) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
Result rc = envSetNextLoad(GAME, GAME);
|
||||||
|
if (R_FAILED(rc)) return 1;
|
||||||
|
return 0;
|
||||||
|
#else
|
||||||
|
fprintf(stderr, "ota-bootstrap: host stub (would rename %s -> %s, load %s)\n", STAGED, LAUNCHER,
|
||||||
|
GAME);
|
||||||
|
return 0;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
@@ -130,7 +130,10 @@ ifneq ($(ROMFS),)
|
|||||||
export NROFLAGS += --romfsdir=$(CURDIR)/$(ROMFS)
|
export NROFLAGS += --romfsdir=$(CURDIR)/$(ROMFS)
|
||||||
endif
|
endif
|
||||||
|
|
||||||
.PHONY: $(BUILD) clean all host-test sync-romfs
|
BOOTSTRAP_DIR := $(TOPDIR)/../switch-ota-bootstrap
|
||||||
|
BOOTSTRAP_ROMFS := $(CURDIR)/$(ROMFS)/ota-bootstrap.nro
|
||||||
|
|
||||||
|
.PHONY: $(BUILD) clean all host-test sync-romfs bootstrap-romfs
|
||||||
|
|
||||||
#---------------------------------------------------------------------------------
|
#---------------------------------------------------------------------------------
|
||||||
all: sync-romfs $(BUILD)
|
all: sync-romfs $(BUILD)
|
||||||
@@ -138,7 +141,12 @@ all: sync-romfs $(BUILD)
|
|||||||
CACERT_URL := https://curl.se/ca/cacert.pem
|
CACERT_URL := https://curl.se/ca/cacert.pem
|
||||||
CACERT_ROMFS := $(CURDIR)/$(ROMFS)/cacert.pem
|
CACERT_ROMFS := $(CURDIR)/$(ROMFS)/cacert.pem
|
||||||
|
|
||||||
sync-romfs:
|
bootstrap-romfs:
|
||||||
|
@$(MAKE) --no-print-directory -C $(BOOTSTRAP_DIR) all
|
||||||
|
@mkdir -p $(CURDIR)/$(ROMFS)
|
||||||
|
@cp -f $(BOOTSTRAP_DIR)/ota-bootstrap.nro $(BOOTSTRAP_ROMFS)
|
||||||
|
|
||||||
|
sync-romfs: bootstrap-romfs
|
||||||
@mkdir -p $(CURDIR)/$(ROMFS)
|
@mkdir -p $(CURDIR)/$(ROMFS)
|
||||||
@cp -f $(LOGO_SRC) $(CURDIR)/$(ROMFS)/logo.png
|
@cp -f $(LOGO_SRC) $(CURDIR)/$(ROMFS)/logo.png
|
||||||
@if ! curl -sfL --time-cond $(CACERT_ROMFS) -o $(CACERT_ROMFS) $(CACERT_URL); then \
|
@if ! curl -sfL --time-cond $(CACERT_ROMFS) -o $(CACERT_ROMFS) $(CACERT_URL); then \
|
||||||
|
|||||||
@@ -68,7 +68,10 @@ Manifest: `scripts/switch/ota_launcher.manifest`.
|
|||||||
## Status / known gaps
|
## Status / known gaps
|
||||||
|
|
||||||
- Zip extraction uses `switch-zziplib` (`ota_unzip.c`) on device.
|
- Zip extraction uses `switch-zziplib` (`ota_unzip.c`) on device.
|
||||||
- OTA replaces launcher + game from the install zip (NACP versions stay aligned).
|
- OTA replaces game + launcher from the install zip (NACP versions stay aligned).
|
||||||
|
The running launcher cannot overwrite its own NRO on sdmc/FAT; a tiny
|
||||||
|
`ota-bootstrap.nro` (embedded in romfs) chainloads once to swap the staged
|
||||||
|
launcher, then loads the game.
|
||||||
- HTTPS uses Mozilla CA bundle in romfs (`cacert.pem`, fetched at build time). `ota_net_init()` mounts romfs before the quiet release check.
|
- HTTPS uses Mozilla CA bundle in romfs (`cacert.pem`, fetched at build time). `ota_net_init()` mounts romfs before the quiet release check.
|
||||||
- Sphaira HOME forwarders cache metadata until reinstalled (see docs/switch-install.md).
|
- Sphaira HOME forwarders cache metadata until reinstalled (see docs/switch-install.md).
|
||||||
- Release runner: `switch-dev` + (`install_devkitpro_deps.sh` **or** Docker)
|
- Release runner: `switch-dev` + (`install_devkitpro_deps.sh` **or** Docker)
|
||||||
|
|||||||
@@ -22,6 +22,12 @@ int ota_fs_atomic_replace_game(const char *install_dir, const char *verified_gam
|
|||||||
int ota_fs_atomic_replace_nro(const char *install_dir, const char *nro_name,
|
int ota_fs_atomic_replace_nro(const char *install_dir, const char *nro_name,
|
||||||
const char *verified_nro, char *err, size_t err_len);
|
const char *verified_nro, char *err, size_t err_len);
|
||||||
|
|
||||||
|
/* Stage a new launcher + copy romfs bootstrap; chainload bootstrap_out next.
|
||||||
|
* The running launcher cannot replace its own NRO on sdmc/FAT. */
|
||||||
|
int ota_fs_stage_launcher_bootstrap(const char *install_dir, const char *verified_launcher,
|
||||||
|
char *bootstrap_out, size_t bootstrap_out_len, char *err,
|
||||||
|
size_t err_len);
|
||||||
|
|
||||||
/* Hand off to game NRO via envSetNextLoad (Switch) or no-op stub (host). */
|
/* Hand off to game NRO via envSetNextLoad (Switch) or no-op stub (host). */
|
||||||
int ota_fs_handoff_to_game(const char *game_nro_path, char *err, size_t err_len);
|
int ota_fs_handoff_to_game(const char *game_nro_path, char *err, size_t err_len);
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ extern "C" {
|
|||||||
#define OTA_LAUNCHER_NRO_NAME "gen1recomp.nro"
|
#define OTA_LAUNCHER_NRO_NAME "gen1recomp.nro"
|
||||||
#define OTA_SAVE_DIR_NAME "pokemon-love2d"
|
#define OTA_SAVE_DIR_NAME "pokemon-love2d"
|
||||||
#define OTA_INSTALL_DIR "switch/gen1recomp"
|
#define OTA_INSTALL_DIR "switch/gen1recomp"
|
||||||
|
#define OTA_LAUNCHER_STAGED_SUFFIX ".staged"
|
||||||
|
#define OTA_BOOTSTRAP_ROMFS "romfs:/ota-bootstrap.nro"
|
||||||
|
#define OTA_BOOTSTRAP_SD_NAME "ota-bootstrap.nro"
|
||||||
#define OTA_RELEASES_API \
|
#define OTA_RELEASES_API \
|
||||||
"https://api.github.com/repos/bryanthaboi/gen1recomp/releases/latest"
|
"https://api.github.com/repos/bryanthaboi/gen1recomp/releases/latest"
|
||||||
|
|
||||||
|
|||||||
@@ -198,12 +198,15 @@ static int run_update_flow(const char *install_dir) {
|
|||||||
"Update zip is missing launcher files.", err, installed);
|
"Update zip is missing launcher files.", err, installed);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
if (ota_fs_atomic_replace_nro(install_dir, OTA_LAUNCHER_NRO_NAME, extracted_launcher, err,
|
|
||||||
sizeof(err)) != 0) {
|
char bootstrap_path[256];
|
||||||
|
bootstrap_path[0] = '\0';
|
||||||
|
if (ota_fs_stage_launcher_bootstrap(install_dir, extracted_launcher, bootstrap_path,
|
||||||
|
sizeof(bootstrap_path), err, sizeof(err)) != 0) {
|
||||||
remove(extracted_launcher);
|
remove(extracted_launcher);
|
||||||
remove(zip_path);
|
remove(zip_path);
|
||||||
show_update_error("Could not install",
|
show_update_error("Could not install",
|
||||||
"Game updated but launcher could not be replaced. Reinstall from the SD zip.",
|
"Game updated but launcher could not be staged. Reinstall from the SD zip.",
|
||||||
err, installed);
|
err, installed);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -217,8 +220,16 @@ static int run_update_flow(const char *install_dir) {
|
|||||||
fclose(vf);
|
fclose(vf);
|
||||||
}
|
}
|
||||||
|
|
||||||
ota_ui_show_progress("Ready", "Starting the game...", 1.0f);
|
ota_ui_show_progress("Ready", "Finishing update...", 1.0f);
|
||||||
svcSleepThread(600000000ULL);
|
svcSleepThread(600000000ULL);
|
||||||
|
remove(zip_path);
|
||||||
|
ota_ui_shutdown();
|
||||||
|
|
||||||
|
if (ota_fs_handoff_to_game(bootstrap_path, err, sizeof(err)) != 0) {
|
||||||
|
show_update_error("Could not install", "Launcher bootstrap failed.", err, installed);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return 2; /* chainload bootstrap; main must not hand off to game */
|
||||||
#else
|
#else
|
||||||
(void)extracted;
|
(void)extracted;
|
||||||
(void)extracted_launcher;
|
(void)extracted_launcher;
|
||||||
@@ -233,18 +244,26 @@ int main(int argc, char **argv) {
|
|||||||
(void)argv;
|
(void)argv;
|
||||||
|
|
||||||
const char *install = SD_INSTALL_DIR;
|
const char *install = SD_INSTALL_DIR;
|
||||||
|
int update_rc = 0;
|
||||||
|
|
||||||
#if defined(__SWITCH__)
|
#if defined(__SWITCH__)
|
||||||
socketInitializeDefault();
|
socketInitializeDefault();
|
||||||
padConfigureInput(1, HidNpadStyleSet_NpadStandard);
|
padConfigureInput(1, HidNpadStyleSet_NpadStandard);
|
||||||
if (ota_net_init() == 0) {
|
if (ota_net_init() == 0) {
|
||||||
run_update_flow(install);
|
update_rc = run_update_flow(install);
|
||||||
ota_net_shutdown();
|
ota_net_shutdown();
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
run_update_flow(install);
|
update_rc = run_update_flow(install);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
if (update_rc == 2) {
|
||||||
|
#if defined(__SWITCH__)
|
||||||
|
socketExit();
|
||||||
|
#endif
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
char game[192];
|
char game[192];
|
||||||
snprintf(game, sizeof(game), "%s/%s", install, OTA_GAME_NRO_NAME);
|
snprintf(game, sizeof(game), "%s/%s", install, OTA_GAME_NRO_NAME);
|
||||||
char err[128];
|
char err[128];
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
#if defined(__SWITCH__)
|
#if defined(__SWITCH__)
|
||||||
#include <switch.h>
|
#include <switch.h>
|
||||||
#include <mbedtls/sha256.h>
|
#include <mbedtls/sha256.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#else
|
#else
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
@@ -114,6 +115,47 @@ int ota_fs_atomic_replace_nro(const char *install_dir, const char *nro_name,
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int ota_fs_stage_launcher_bootstrap(const char *install_dir, const char *verified_launcher,
|
||||||
|
char *bootstrap_out, size_t bootstrap_out_len, char *err,
|
||||||
|
size_t err_len) {
|
||||||
|
if (!verified_launcher || !*verified_launcher) {
|
||||||
|
if (err && err_len) snprintf(err, err_len, "missing staged launcher path");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (!bootstrap_out || bootstrap_out_len == 0) {
|
||||||
|
if (err && err_len) snprintf(err, err_len, "missing bootstrap output buffer");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
bootstrap_out[0] = '\0';
|
||||||
|
|
||||||
|
const char *base = install_dir ? install_dir : OTA_INSTALL_DIR;
|
||||||
|
char staged[256];
|
||||||
|
char bootstrap[256];
|
||||||
|
char updates[192];
|
||||||
|
snprintf(staged, sizeof(staged), "%s/%s%s", base, OTA_LAUNCHER_NRO_NAME,
|
||||||
|
OTA_LAUNCHER_STAGED_SUFFIX);
|
||||||
|
snprintf(updates, sizeof(updates), "%s/updates", base);
|
||||||
|
snprintf(bootstrap, sizeof(bootstrap), "%s/updates/%s", base, OTA_BOOTSTRAP_SD_NAME);
|
||||||
|
|
||||||
|
remove(staged);
|
||||||
|
if (copy_file(verified_launcher, staged) != 0) {
|
||||||
|
if (err && err_len) snprintf(err, err_len, "stage launcher failed");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#if defined(__SWITCH__)
|
||||||
|
mkdir(updates, 0755);
|
||||||
|
#endif
|
||||||
|
if (copy_file(OTA_BOOTSTRAP_ROMFS, bootstrap) != 0) {
|
||||||
|
remove(staged);
|
||||||
|
if (err && err_len) snprintf(err, err_len, "extract bootstrap failed");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
snprintf(bootstrap_out, bootstrap_out_len, "%s", bootstrap);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
int ota_fs_handoff_to_game(const char *game_nro_path, char *err, size_t err_len) {
|
int ota_fs_handoff_to_game(const char *game_nro_path, char *err, size_t err_len) {
|
||||||
if (!game_nro_path || !*game_nro_path) {
|
if (!game_nro_path || !*game_nro_path) {
|
||||||
if (err && err_len) snprintf(err, err_len, "missing game path");
|
if (err && err_len) snprintf(err, err_len, "missing game path");
|
||||||
|
|||||||
Reference in New Issue
Block a user