Preserve.ini for update cleanup, Hekate UI strings, docs
All checks were successful
Build / Build (push) Successful in 17s

- Load sd:/config/omninx/preserve.ini during update mode cleanup to skip
  delete paths that overlap user-preserved prefixes (heap-backed path list).
- Document preserve.ini in PRESERVE_INI.md, README, and INSTALLATION_PROCESS.
- Prefer Hekate wording in main.c user messages for bootloader launch/errors.
- Extend update deletion list for exefs_patches (audio_mastervolume, SaltyNX_Fixes, logo).
This commit is contained in:
2026-05-17 11:21:10 +02:00
parent dce11538d1
commit 59f103874c
8 changed files with 248 additions and 29 deletions

View File

@@ -90,7 +90,9 @@ The OmniNX Installer Payload operates in two modes:
**Trigger**: `INSTALL_MODE_UPDATE` (OmniNX already installed)
### Step 1: Cleanup (Selective Deletion)
**Location**: `install.c:321-380`
**Location**: `install_update.c` (`update_mode_cleanup`), `install.c` (`delete_path_lists_grouped`)
Before deletion, the payload reads optional **`sd:/config/omninx/preserve.ini`** (section `[Preserve]`). Paths enabled there are skipped, as are delete targets that would remove them (parent/child overlap). See **[PRESERVE_INI.md](PRESERVE_INI.md)**. Clean install does not use this file.
#### 10. Clean Atmosphere Subdirectories
**Location**: `install.c:325-327`, `deletion_lists.h:9-34`

59
PRESERVE_INI.md Normal file
View File

@@ -0,0 +1,59 @@
# User preserve list (`preserve.ini`)
**Update mode only.** During OmniNX update cleanup (selective deletion), any path listed here with a truthy value is **not** deleted. Paths that would remove a preserved path (parent directory) are skipped as well.
- **File:** `sd:/config/omninx/preserve.ini`
- If missing or invalid, behavior is unchanged (full update deletion list applies).
## Format
Section **`[Preserve]`** (case-sensitive). Each line is **path as key**, **enable flag as value**:
```ini
[Preserve]
'sd:/ROMs'=1
sd:/switch/MyHomebrewFolder=0
sd:/backup/tools=1
```
### Path (key)
- Must start with **`sd:/`** (after optional quoting).
- **Optional** matching **single** or **double** quotes around the key are stripped (helps Ultrahand `set-ini-val` and readability).
- Trailing slashes are normalized away (except the volume root).
- **`sd:/` alone** is rejected (too broad).
- Minimum useful length after `sd:/` is enforced (at least one more character).
### Value (enable)
**On** if the value starts with `1`, `t`, `T`, `y`, or `Y` (e.g. `1`, `true`, `yes`). Anything else is **off** (path ignored).
### Limits
- Up to **96** preserved paths; longer keys truncated internally (~224 bytes per path).
- Duplicate paths after normalization are skipped.
- The list is allocated on the **heap** only while update cleanup runs (not a large static buffer in IPL IRAM), to avoid crowding hardware-adjacent memory used by Joy-Con UART and similar peripherals.
## Overlap rules
For each delete candidate from `deletion_lists_update.h`, deletion is skipped if it **overlaps** any enabled preserve path:
- Same path, or
- Delete target is **inside** a preserved tree (preserved path is a prefix), or
- Delete target is an **ancestor** of a preserved path (deleting it would remove the preserved data).
So preserving `sd:/ROMs` also blocks deleting `sd:/ROMs/title` if that were on the delete list, and blocks deleting `sd:/` if `sd:/ROMs` is preserved.
## Ultrahand
You can toggle entries with:
```text
set-ini-val sd:/config/omninx/preserve.ini Preserve 'sd:/ROMs' 1
```
and `0` to disable. Use the same quoted path in the key argument as in your INI.
## Clean install
**Not used** in clean install mode. `preserve.ini` is only read for **update** cleanup (`update_mode_cleanup`).

View File

@@ -23,12 +23,14 @@ Based on [HATS-Installer-Payload](https://github.com/sthetix/HATS-Installer-Payl
For detailed information about the installation process, see:
- **[INSTALLATION_PROCESS.md](INSTALLATION_PROCESS.md)** - Complete step-by-step breakdown of everything checked and done during installation/update
- **[DEBUG_INI.md](DEBUG_INI.md)** - Optional `sd:/config/omninx/debug.ini` parameters to skip install steps (for testing)
- **[PRESERVE_INI.md](PRESERVE_INI.md)** - Optional `sd:/config/omninx/preserve.ini` to skip deleting chosen paths in **update** mode
## Installation Modes
### Update Mode (OmniNX Detected)
- Detected when version marker files (`1.0.0s`, `1.0.0l`, or `1.0.0oc`) are found
- Performs selective deletion of specific directories/files
- Optional **`sd:/config/omninx/preserve.ini`**: user-listed `sd:/…` paths (with `=1`) are excluded from that cleanup (see [PRESERVE_INI.md](PRESERVE_INI.md))
- Preserves user data, savegames, and installed games
- Updates only necessary CFW components

View File

@@ -10,14 +10,16 @@ static const char* atmosphere_dirs_to_delete[] = {
"sd:/atmosphere/config",
"sd:/atmosphere/crash_reports",
"sd:/atmosphere/erpt_reports",
"sd:/atmosphere/exefs_patches/audio_mastervolume",
"sd:/atmosphere/exefs_patches/CrunchPatch",
"sd:/atmosphere/exefs_patches/SaltyNX_Fixes",
"sd:/atmosphere/exefs_patches/Crunchyroll Patch 1.10.0",
"sd:/atmosphere/exefs_patches/bluetooth_patches",
"sd:/atmosphere/exefs_patches/bootlogo",
"sd:/atmosphere/exefs_patches/btm_patches",
"sd:/atmosphere/exefs_patches/es_patches",
"sd:/atmosphere/exefs_patches/hid_patches",
"sd:/atmosphere/exefs_patches/logo1",
"sd:/atmosphere/exefs_patches/logo",
"sd:/atmosphere/exefs_patches/nfim_ctest",
"sd:/atmosphere/exefs_patches/nim_ctest",
"sd:/atmosphere/exefs_patches/nvnflinger_cmu",

View File

@@ -21,6 +21,8 @@
#define GROUPED_DELETE_MAX_ENTRIES 512
static bool install_path_blocked_by_user_preserve(const char *path);
#ifndef VERSION
#define VERSION "1.0.0"
#endif
@@ -463,7 +465,7 @@ int delete_path_lists_grouped(const char *folder_display_name, ...) {
const char **list;
while (n_entries < GROUPED_DELETE_MAX_ENTRIES && (list = va_arg(ap, const char **)) != NULL) {
for (int i = 0; list[i] != NULL && n_entries < GROUPED_DELETE_MAX_ENTRIES; i++) {
if (install_path_exists(list[i]))
if (install_path_exists(list[i]) && !install_path_blocked_by_user_preserve(list[i]))
entries[n_entries++] = list[i];
}
}
@@ -538,9 +540,9 @@ int delete_path_list(const char* paths[], const char* description) {
u32 start_x, start_y;
int last_percent = -1;
// Count total paths first
// Count total paths first (only those we will delete)
for (int i = 0; paths[i] != NULL; i++) {
if (install_path_exists(paths[i])) {
if (install_path_exists(paths[i]) && !install_path_blocked_by_user_preserve(paths[i])) {
total_paths++;
}
}
@@ -558,7 +560,7 @@ int delete_path_list(const char* paths[], const char* description) {
install_set_color(COLOR_WHITE);
for (int i = 0; paths[i] != NULL; i++) {
if (install_path_exists(paths[i])) {
if (install_path_exists(paths[i]) && !install_path_blocked_by_user_preserve(paths[i])) {
FILINFO fno;
f_stat(paths[i], &fno);
@@ -686,6 +688,159 @@ static void ram_config_free_sections(link_t *sections)
}
}
#define INSTALL_PRESERVE_INI "sd:/config/omninx/preserve.ini"
#define INSTALL_PRESERVE_MAX 96
#define INSTALL_PRESERVE_PATH_BYTES 224
static bool install_ini_kv_truth(const char *val)
{
if (!val || !val[0])
return false;
char c = val[0];
return c == '1' || c == 't' || c == 'T' || c == 'y' || c == 'Y';
}
static void preserve_strip_outer_quotes(char *s)
{
size_t n = strlen(s);
if (n >= 2 &&
((s[0] == '\'' && s[n - 1] == '\'') || (s[0] == '"' && s[n - 1] == '"'))) {
memmove(s, s + 1, n - 2);
s[n - 2] = '\0';
}
}
static void preserve_normalize_trailing_slashes(char *s)
{
size_t n = strlen(s);
while (n > 1 && s[n - 1] == '/')
s[--n] = '\0';
}
static bool path_is_same_or_descendant(const char *base, const char *path)
{
size_t lb = strlen(base);
if (strncmp(path, base, lb) != 0)
return false;
if (path[lb] == '\0')
return true;
if (path[lb] == '/')
return true;
if (lb == 4 && base[0] == 's' && base[1] == 'd' && base[2] == ':' && base[3] == '/')
return path[lb] != '\0';
return false;
}
static bool path_delete_overlaps_preserve(const char *del, const char *pres)
{
return path_is_same_or_descendant(pres, del) || path_is_same_or_descendant(del, pres);
}
static bool g_preserve_update_active;
/** Rows stored in DRAM (heap); avoids ~21KB BSS in tight IPL IRAM around 0x40008000. */
static char *g_preserve_block;
static int g_preserve_count;
static const char *preserve_get_row(int i)
{
return g_preserve_block + (size_t)i * INSTALL_PRESERVE_PATH_BYTES;
}
static bool install_path_blocked_by_user_preserve(const char *delete_path)
{
if (!g_preserve_update_active || !g_preserve_block || !delete_path)
return false;
for (int i = 0; i < g_preserve_count; i++) {
if (path_delete_overlaps_preserve(delete_path, preserve_get_row(i)))
return true;
}
return false;
}
void install_preserve_update_cleanup_begin(void)
{
if (g_preserve_block) {
free(g_preserve_block);
g_preserve_block = NULL;
}
g_preserve_count = 0;
g_preserve_update_active = false;
if (!install_path_exists(INSTALL_PRESERVE_INI))
return;
g_preserve_block = calloc(INSTALL_PRESERVE_MAX, INSTALL_PRESERVE_PATH_BYTES);
if (!g_preserve_block)
return;
link_t sections;
list_init(&sections);
if (ini_parse(&sections, (char *)INSTALL_PRESERVE_INI, false) != 1) {
ram_config_free_sections(&sections);
free(g_preserve_block);
g_preserve_block = NULL;
return;
}
LIST_FOREACH_ENTRY(ini_sec_t, sec, &sections, link) {
if (!sec->name || strcmp(sec->name, "Preserve") != 0)
continue;
LIST_FOREACH_ENTRY(ini_kv_t, kv, &sec->kvs, link) {
if (!kv->key || !kv->val || g_preserve_count >= INSTALL_PRESERVE_MAX)
continue;
if (!install_ini_kv_truth(kv->val))
continue;
char buf[INSTALL_PRESERVE_PATH_BYTES];
strncpy(buf, kv->key, sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0';
preserve_strip_outer_quotes(buf);
preserve_normalize_trailing_slashes(buf);
if (strlen(buf) < 5 || strncmp(buf, "sd:/", 4) != 0)
continue;
if (strcmp(buf, "sd:/") == 0)
continue;
bool dup = false;
for (int j = 0; j < g_preserve_count; j++) {
if (strcmp(preserve_get_row(j), buf) == 0) {
dup = true;
break;
}
}
if (dup)
continue;
char *row = (char *)preserve_get_row(g_preserve_count);
strncpy(row, buf, INSTALL_PRESERVE_PATH_BYTES - 1);
row[INSTALL_PRESERVE_PATH_BYTES - 1] = '\0';
g_preserve_count++;
}
break;
}
ram_config_free_sections(&sections);
if (g_preserve_count == 0) {
free(g_preserve_block);
g_preserve_block = NULL;
return;
}
g_preserve_update_active = true;
install_set_color(COLOR_CYAN);
gfx_printf(" preserve.ini: %d Pfad(e) von Update-Bereinigung ausgenommen\n", g_preserve_count);
install_set_color(COLOR_WHITE);
}
void install_preserve_update_cleanup_end(void)
{
if (g_preserve_block) {
free(g_preserve_block);
g_preserve_block = NULL;
}
g_preserve_count = 0;
g_preserve_update_active = false;
}
/* Valid [Ram] 8gb=0|1 → silent; missing file or invalid → menu. */
static int read_ram_config_silent(const char *path, bool *use_8gb)
{
@@ -884,14 +1039,6 @@ typedef struct {
bool skip_hekate_8gb_post_copy;
} install_debug_opts_t;
static bool install_ini_truth(const char *val)
{
if (!val || !val[0])
return false;
char c = val[0];
return c == '1' || c == 't' || c == 'T' || c == 'y' || c == 'Y';
}
static void install_debug_load(install_debug_opts_t *d)
{
memset(d, 0, sizeof(*d));
@@ -912,25 +1059,25 @@ static void install_debug_load(install_debug_opts_t *d)
if (!kv->key || !kv->val)
continue;
if (strcmp(kv->key, "skip_clean_backup") == 0)
d->skip_clean_backup = install_ini_truth(kv->val);
d->skip_clean_backup = install_ini_kv_truth(kv->val);
else if (strcmp(kv->key, "skip_clean_wipe") == 0)
d->skip_clean_wipe = install_ini_truth(kv->val);
d->skip_clean_wipe = install_ini_kv_truth(kv->val);
else if (strcmp(kv->key, "skip_clean_restore") == 0)
d->skip_clean_restore = install_ini_truth(kv->val);
d->skip_clean_restore = install_ini_kv_truth(kv->val);
else if (strcmp(kv->key, "skip_clean_install") == 0)
d->skip_clean_install = install_ini_truth(kv->val);
d->skip_clean_install = install_ini_kv_truth(kv->val);
else if (strcmp(kv->key, "skip_clean_staging_cleanup") == 0)
d->skip_clean_staging_cleanup = install_ini_truth(kv->val);
d->skip_clean_staging_cleanup = install_ini_kv_truth(kv->val);
else if (strcmp(kv->key, "skip_update_cleanup") == 0)
d->skip_update_cleanup = install_ini_truth(kv->val);
d->skip_update_cleanup = install_ini_kv_truth(kv->val);
else if (strcmp(kv->key, "skip_update_install") == 0)
d->skip_update_install = install_ini_truth(kv->val);
d->skip_update_install = install_ini_kv_truth(kv->val);
else if (strcmp(kv->key, "skip_update_staging_cleanup") == 0)
d->skip_update_staging_cleanup = install_ini_truth(kv->val);
d->skip_update_staging_cleanup = install_ini_kv_truth(kv->val);
else if (strcmp(kv->key, "skip_update_horizon_oc") == 0)
d->skip_update_horizon_oc = install_ini_truth(kv->val);
d->skip_update_horizon_oc = install_ini_kv_truth(kv->val);
else if (strcmp(kv->key, "skip_hekate_8gb_post_copy") == 0)
d->skip_hekate_8gb_post_copy = install_ini_truth(kv->val);
d->skip_hekate_8gb_post_copy = install_ini_kv_truth(kv->val);
}
break;
}

View File

@@ -32,6 +32,9 @@ int clean_mode_wipe(void);
int clean_mode_restore(void);
int clean_mode_install(omninx_variant_t variant);
void install_preserve_update_cleanup_begin(void);
void install_preserve_update_cleanup_end(void);
// Shared helpers (install.c) - used by install_update.c and install_clean.c
void install_set_color(u32 color);
void install_check_and_clear_screen_if_needed(void);

View File

@@ -37,6 +37,8 @@
// Update mode: Cleanup specific directories/files
int update_mode_cleanup(omninx_variant_t variant) {
(void)variant;
install_preserve_update_cleanup_begin();
check_and_clear_screen_if_needed();
set_color(COLOR_WHITE);
@@ -66,6 +68,8 @@ int update_mode_cleanup(omninx_variant_t variant) {
misc_files_to_delete,
NULL);
install_preserve_update_cleanup_end();
set_color(COLOR_GREEN);
gfx_printf(" Bereinigung abgeschlossen!\n");
set_color(COLOR_WHITE);

View File

@@ -145,7 +145,7 @@ static int launch_payload(const char *path) {
if (sd_mount()) {
FIL fp;
if (f_open(&fp, path, FA_READ)) {
gfx_printf("Payload nicht gefunden: %s\n", path);
gfx_printf("Hekate nicht gefunden: %s\n", path);
return 1;
}
@@ -301,7 +301,7 @@ void ipl_main(void) {
if (file_exists(PAYLOAD_PATH)) {
gfx_printf("\n");
set_color(COLOR_CYAN);
gfx_printf("Payload wird gestartet...\n");
gfx_printf("Hekate wird gestartet...\n");
set_color(COLOR_WHITE);
msleep(500);
launch_payload(PAYLOAD_PATH);
@@ -721,16 +721,16 @@ void ipl_main(void) {
gfx_printf("\n");
set_color(COLOR_CYAN);
gfx_printf("Payload wird gestartet...\n");
gfx_printf("Hekate wird gestartet...\n");
set_color(COLOR_WHITE);
msleep(500); // Brief delay before launch
if (file_exists(PAYLOAD_PATH)) {
launch_payload(PAYLOAD_PATH);
} else {
// Payload not found, show error
// Hekate (update.bin) not found, show error
set_color(COLOR_RED);
gfx_printf("\nFEHLER: Payload nicht gefunden!\n");
gfx_printf("\nFEHLER: Hekate nicht gefunden!\n");
gfx_printf("Pfad: %s\n", PAYLOAD_PATH);
set_color(COLOR_WHITE);
gfx_printf("\nDruecke A oder Power zum Neustart...\n");