diff --git a/.gitignore b/.gitignore index b237d83c..6f8443fb 100644 --- a/.gitignore +++ b/.gitignore @@ -46,6 +46,12 @@ native/switch-ota-launcher/*.elf native/switch-ota-launcher/*.map native/switch-ota-launcher/romfs/logo.png 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/) mobile/dist/ diff --git a/native/switch-ota-bootstrap/Makefile b/native/switch-ota-bootstrap/Makefile new file mode 100644 index 00000000..4920b3c3 --- /dev/null +++ b/native/switch-ota-bootstrap/Makefile @@ -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 diff --git a/native/switch-ota-bootstrap/main.c b/native/switch-ota-bootstrap/main.c new file mode 100644 index 00000000..0aec9352 --- /dev/null +++ b/native/switch-ota-bootstrap/main.c @@ -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 +#include +#include + +#if defined(__SWITCH__) +#include +#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 +} diff --git a/native/switch-ota-launcher/Makefile b/native/switch-ota-launcher/Makefile index 6c7b463c..94d368b3 100644 --- a/native/switch-ota-launcher/Makefile +++ b/native/switch-ota-launcher/Makefile @@ -130,7 +130,10 @@ ifneq ($(ROMFS),) export NROFLAGS += --romfsdir=$(CURDIR)/$(ROMFS) 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) @@ -138,7 +141,12 @@ all: sync-romfs $(BUILD) CACERT_URL := https://curl.se/ca/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) @cp -f $(LOGO_SRC) $(CURDIR)/$(ROMFS)/logo.png @if ! curl -sfL --time-cond $(CACERT_ROMFS) -o $(CACERT_ROMFS) $(CACERT_URL); then \ diff --git a/native/switch-ota-launcher/README.md b/native/switch-ota-launcher/README.md index 96417bcc..9879e554 100644 --- a/native/switch-ota-launcher/README.md +++ b/native/switch-ota-launcher/README.md @@ -68,7 +68,10 @@ Manifest: `scripts/switch/ota_launcher.manifest`. ## Status / known gaps - 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. - Sphaira HOME forwarders cache metadata until reinstalled (see docs/switch-install.md). - Release runner: `switch-dev` + (`install_devkitpro_deps.sh` **or** Docker) diff --git a/native/switch-ota-launcher/include/ota_fs.h b/native/switch-ota-launcher/include/ota_fs.h index 4a43968e..f94d98bd 100644 --- a/native/switch-ota-launcher/include/ota_fs.h +++ b/native/switch-ota-launcher/include/ota_fs.h @@ -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, 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). */ int ota_fs_handoff_to_game(const char *game_nro_path, char *err, size_t err_len); diff --git a/native/switch-ota-launcher/include/ota_protocol.h b/native/switch-ota-launcher/include/ota_protocol.h index 5e24535e..995efd7c 100644 --- a/native/switch-ota-launcher/include/ota_protocol.h +++ b/native/switch-ota-launcher/include/ota_protocol.h @@ -12,6 +12,9 @@ extern "C" { #define OTA_LAUNCHER_NRO_NAME "gen1recomp.nro" #define OTA_SAVE_DIR_NAME "pokemon-love2d" #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 \ "https://api.github.com/repos/bryanthaboi/gen1recomp/releases/latest" diff --git a/native/switch-ota-launcher/src/main.c b/native/switch-ota-launcher/src/main.c index e2ed7840..f187e421 100644 --- a/native/switch-ota-launcher/src/main.c +++ b/native/switch-ota-launcher/src/main.c @@ -198,12 +198,15 @@ static int run_update_flow(const char *install_dir) { "Update zip is missing launcher files.", err, installed); 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(zip_path); 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); return 0; } @@ -217,8 +220,16 @@ static int run_update_flow(const char *install_dir) { fclose(vf); } - ota_ui_show_progress("Ready", "Starting the game...", 1.0f); + ota_ui_show_progress("Ready", "Finishing update...", 1.0f); 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 (void)extracted; (void)extracted_launcher; @@ -233,18 +244,26 @@ int main(int argc, char **argv) { (void)argv; const char *install = SD_INSTALL_DIR; + int update_rc = 0; #if defined(__SWITCH__) socketInitializeDefault(); padConfigureInput(1, HidNpadStyleSet_NpadStandard); if (ota_net_init() == 0) { - run_update_flow(install); + update_rc = run_update_flow(install); ota_net_shutdown(); } #else - run_update_flow(install); + update_rc = run_update_flow(install); #endif + if (update_rc == 2) { +#if defined(__SWITCH__) + socketExit(); +#endif + return 0; + } + char game[192]; snprintf(game, sizeof(game), "%s/%s", install, OTA_GAME_NRO_NAME); char err[128]; diff --git a/native/switch-ota-launcher/src/ota_fs.c b/native/switch-ota-launcher/src/ota_fs.c index 15d9631b..5adca96b 100644 --- a/native/switch-ota-launcher/src/ota_fs.c +++ b/native/switch-ota-launcher/src/ota_fs.c @@ -7,6 +7,7 @@ #if defined(__SWITCH__) #include #include +#include #include #else #include @@ -114,6 +115,47 @@ int ota_fs_atomic_replace_nro(const char *install_dir, const char *nro_name, 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) { if (!game_nro_path || !*game_nro_path) { if (err && err_len) snprintf(err, err_len, "missing game path");