feat: redesign installer on MTK toolkit with component selection
Rewrite the Installer app on the Montauk Toolkit (Canvas + gui/mtk widgets, theme, hover states, scrollbar) replacing the hand-rolled px_* renderer and bespoke TrueType usage. Add a new "Software" step: an expandable checkbox tree for optional components (Montauk SDK with gcc/g++, binutils, tcc, lua sub-items; Games; Office; Internet apps; Printing; experimental httpd and SDR). Unchecked components' paths are excluded from the install copy, with a longest-path ownership rule so e.g. sdk/tcc installs even when the rest of the SDK is deselected. The update flow refreshes only the components the target already has. Add tri-state checkbox and disclosure-arrow widgets to the MTK toolkit (anti-aliased, supersampled). Merge tcc and lua into 0:/sdk (0:/sdk/tcc, 0:/sdk/lua) and drop 0:/lib: update tcc config.h, lua luaconf.h, both Makefiles, the devkit clean scope, man pages and montaukos.org notices. Make printing optional: init skips printd when 0:/os/printd.elf is absent. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
@@ -81,6 +81,96 @@ static void path_join(char* out, int outsize, const char* dir, const char* name)
|
||||
out[i] = '\0';
|
||||
}
|
||||
|
||||
static const char* rel_basename(const char* rel) {
|
||||
const char* base = rel;
|
||||
for (int i = 0; rel[i]; i++)
|
||||
if (rel[i] == '/') base = rel + i + 1;
|
||||
return base;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Filesystem probes and heap-backed directory listing
|
||||
// ============================================================================
|
||||
|
||||
bool installer_path_exists(const char* abs_path) {
|
||||
int fd = montauk::open(abs_path);
|
||||
if (fd >= 0) {
|
||||
montauk::close(fd);
|
||||
return true;
|
||||
}
|
||||
const char* names[4];
|
||||
return montauk::readdir_at(abs_path, names, 4, 0) >= 0;
|
||||
}
|
||||
|
||||
void installer_free_dir_list(DirList* list) {
|
||||
if (!list) return;
|
||||
for (int i = 0; i < list->count; i++)
|
||||
montauk::mfree(list->entries[i].rel);
|
||||
montauk::mfree(list->entries);
|
||||
list->entries = nullptr;
|
||||
list->count = 0;
|
||||
}
|
||||
|
||||
// The kernel serves readdir strings from a small ring of scratch pages, so
|
||||
// nested listings invalidate earlier results. Copy each batch to the heap
|
||||
// and page with readdir_at so directories of any size enumerate fully.
|
||||
bool installer_list_dir(const char* abs_dir, DirList* out) {
|
||||
out->entries = nullptr;
|
||||
out->count = 0;
|
||||
|
||||
static constexpr int BATCH = 128;
|
||||
const char* batch[BATCH];
|
||||
|
||||
int scanned = 0;
|
||||
int capacity = 0;
|
||||
|
||||
int got = montauk::readdir_at(abs_dir, batch, BATCH, 0);
|
||||
if (got < 0) return false;
|
||||
|
||||
while (got > 0) {
|
||||
for (int i = 0; i < got; i++) {
|
||||
const char* raw = batch[i];
|
||||
int len = slen(raw);
|
||||
|
||||
bool is_dir = (len > 0 && raw[len - 1] == '/');
|
||||
if (is_dir) len--;
|
||||
if (len <= 0) continue;
|
||||
|
||||
// Skip "." and ".."
|
||||
const char* base = raw;
|
||||
for (int k = 0; k < len; k++)
|
||||
if (raw[k] == '/') base = raw + k + 1;
|
||||
int base_len = (int)(len - (base - raw));
|
||||
if (base_len == 1 && base[0] == '.') continue;
|
||||
if (base_len == 2 && base[0] == '.' && base[1] == '.') continue;
|
||||
|
||||
if (out->count >= capacity) {
|
||||
int new_cap = capacity ? capacity * 2 : 64;
|
||||
DirEntry* grown = (DirEntry*)montauk::malloc(new_cap * sizeof(DirEntry));
|
||||
if (!grown) { installer_free_dir_list(out); return false; }
|
||||
for (int k = 0; k < out->count; k++) grown[k] = out->entries[k];
|
||||
montauk::mfree(out->entries);
|
||||
out->entries = grown;
|
||||
capacity = new_cap;
|
||||
}
|
||||
|
||||
char* rel = (char*)montauk::malloc(len + 1);
|
||||
if (!rel) { installer_free_dir_list(out); return false; }
|
||||
montauk::memcpy(rel, raw, len);
|
||||
rel[len] = '\0';
|
||||
|
||||
out->entries[out->count].rel = rel;
|
||||
out->entries[out->count].is_dir = is_dir;
|
||||
out->count++;
|
||||
}
|
||||
|
||||
scanned += got;
|
||||
got = montauk::readdir_at(abs_dir, batch, BATCH, scanned);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Copy a single file from src_path to dst_path
|
||||
// ============================================================================
|
||||
@@ -159,113 +249,78 @@ static bool copy_file(const char* src_path, const char* dst_path) {
|
||||
static int g_files_copied;
|
||||
static int g_dirs_created;
|
||||
|
||||
// Copy all entries from src_dir (on ramdisk) to dst_dir (on target drive).
|
||||
// Copy all entries from src_dir (on ramdisk) to dst_dir (on target drive),
|
||||
// honoring the component selection: paths owned by an unchecked component
|
||||
// are skipped, and directories are still entered when a checked component
|
||||
// lives deeper inside (e.g. sdk/tcc with the rest of sdk deselected).
|
||||
// If skip_toplevel is non-null, skip that directory name at the top level.
|
||||
static bool copy_recursive(const char* src_dir, const char* dst_dir,
|
||||
const char* skip_toplevel = nullptr) {
|
||||
const char* names[256];
|
||||
int count = montauk::readdir(src_dir, names, 256);
|
||||
if (count < 0) return true; // not a directory or empty, skip
|
||||
DirList list;
|
||||
if (!installer_list_dir(src_dir, &list))
|
||||
return true; // not a directory, skip
|
||||
|
||||
// Ramdisk readdir returns names relative to the drive root with the
|
||||
// full internal path (e.g., for readdir("0:/os"), names come back as
|
||||
// "os/init.elf", "os/shell.elf", etc.). We need to strip the prefix
|
||||
// that corresponds to the local path portion of src_dir.
|
||||
//
|
||||
// Find the local path after the "N:/" prefix.
|
||||
const char* src_local = src_dir;
|
||||
for (int k = 0; src_local[k]; k++) {
|
||||
if (src_local[k] == ':') {
|
||||
src_local += k + 1;
|
||||
if (src_local[0] == '/') src_local++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
int prefix_len = slen(src_local);
|
||||
// If prefix is non-empty, we also skip the trailing '/'
|
||||
if (prefix_len > 0 && src_local[prefix_len - 1] != '/') prefix_len++;
|
||||
bool ok = true;
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
const char* raw_name = names[i];
|
||||
for (int i = 0; i < list.count && ok; i++) {
|
||||
const char* rel = list.entries[i].rel;
|
||||
const char* base = rel_basename(rel);
|
||||
|
||||
// Strip prefix to get basename
|
||||
const char* basename = raw_name;
|
||||
if (prefix_len > 0 && slen(raw_name) > prefix_len) {
|
||||
basename = raw_name + prefix_len;
|
||||
}
|
||||
|
||||
// Skip "." and ".."
|
||||
if (basename[0] == '.' && (basename[1] == '\0' || basename[1] == '/')) continue;
|
||||
if (basename[0] == '.' && basename[1] == '.' && (basename[2] == '\0' || basename[2] == '/')) continue;
|
||||
|
||||
int blen = slen(basename);
|
||||
|
||||
// Check if this is a directory (trailing '/')
|
||||
bool is_dir = (blen > 0 && basename[blen - 1] == '/');
|
||||
|
||||
if (is_dir) {
|
||||
// Strip trailing '/' for the name
|
||||
char dir_name[256];
|
||||
int j = 0;
|
||||
for (; j < blen - 1 && j < 255; j++) dir_name[j] = basename[j];
|
||||
dir_name[j] = '\0';
|
||||
char src_path[256];
|
||||
snprintf(src_path, sizeof(src_path), "0:/%s", rel);
|
||||
|
||||
if (list.entries[i].is_dir) {
|
||||
// Skip the installer app — no need on the installed system
|
||||
if (strcmp(dir_name, "installer") == 0 &&
|
||||
strcmp(src_local, "apps") == 0) continue;
|
||||
if (strcmp(rel, "apps/installer") == 0) continue;
|
||||
|
||||
// Skip requested top-level directory
|
||||
if (skip_toplevel && strcmp(dir_name, skip_toplevel) == 0) continue;
|
||||
if (skip_toplevel && strcmp(base, skip_toplevel) == 0) continue;
|
||||
|
||||
bool allowed = component_allows(rel);
|
||||
if (!allowed && !component_subtree_needed(rel)) continue;
|
||||
|
||||
// Create directory on target
|
||||
char target_path[256];
|
||||
path_join(target_path, sizeof(target_path), dst_dir, dir_name);
|
||||
path_join(target_path, sizeof(target_path), dst_dir, base);
|
||||
|
||||
montauk::fmkdir(target_path);
|
||||
g_dirs_created++;
|
||||
|
||||
char log_msg[64];
|
||||
snprintf(log_msg, sizeof(log_msg), " mkdir %s", dir_name);
|
||||
snprintf(log_msg, sizeof(log_msg), " mkdir %s", base);
|
||||
add_log(log_msg);
|
||||
flush_ui();
|
||||
|
||||
// Recurse into this directory
|
||||
char src_subdir[256];
|
||||
path_join(src_subdir, sizeof(src_subdir), src_dir, dir_name);
|
||||
|
||||
if (!copy_recursive(src_subdir, target_path))
|
||||
return false;
|
||||
ok = copy_recursive(src_path, target_path);
|
||||
} else {
|
||||
// Skip ramdisk and limine.conf — installed system boots from
|
||||
// disk and gets a fresh config without the ramdisk module.
|
||||
// Skip setup.toml — live/setup environment config that should
|
||||
// not be present on the installed system.
|
||||
if (strcmp(basename, "ramdisk.tar") == 0) continue;
|
||||
if (strcmp(basename, "limine.conf") == 0) continue;
|
||||
if (strcmp(basename, "setup.toml") == 0) continue;
|
||||
if (strcmp(base, "ramdisk.tar") == 0) continue;
|
||||
if (strcmp(base, "limine.conf") == 0) continue;
|
||||
if (strcmp(base, "setup.toml") == 0) continue;
|
||||
|
||||
// It's a file — copy it
|
||||
char src_path[256];
|
||||
// Build source path: drive prefix + raw_name (which is the full internal path)
|
||||
// src_dir starts with "0:/" so we need "0:/" + raw_name
|
||||
snprintf(src_path, sizeof(src_path), "0:/%s", raw_name);
|
||||
if (!component_allows(rel)) continue;
|
||||
|
||||
char dst_path[256];
|
||||
path_join(dst_path, sizeof(dst_path), dst_dir, basename);
|
||||
path_join(dst_path, sizeof(dst_path), dst_dir, base);
|
||||
|
||||
char log_msg[64];
|
||||
snprintf(log_msg, sizeof(log_msg), " copy %s", basename);
|
||||
snprintf(log_msg, sizeof(log_msg), " copy %s", base);
|
||||
add_log(log_msg);
|
||||
flush_ui();
|
||||
|
||||
if (!copy_file(src_path, dst_path))
|
||||
return false;
|
||||
if (!copy_file(src_path, dst_path)) {
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
|
||||
g_files_copied++;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
installer_free_dir_list(&list);
|
||||
return ok;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -412,8 +467,7 @@ static void install_efi_ext2(int disk) {
|
||||
add_log(" Mounted");
|
||||
flush_ui();
|
||||
|
||||
char efi_root[8], ext2_root[8];
|
||||
snprintf(efi_root, sizeof(efi_root), "%d:/", efi_drive);
|
||||
char ext2_root[8];
|
||||
snprintf(ext2_root, sizeof(ext2_root), "%d:/", root_drive);
|
||||
|
||||
// Step 8: Create EFI boot directory structure
|
||||
@@ -705,13 +759,14 @@ void do_update() {
|
||||
add_log("ERROR: Mount failed");
|
||||
flush_ui(); st.step = STEP_ERROR; return;
|
||||
}
|
||||
|
||||
char drive_root[8];
|
||||
snprintf(drive_root, sizeof(drive_root), "%d:/", drive_num);
|
||||
add_log(" Mounted");
|
||||
flush_ui();
|
||||
|
||||
// Step 2: Update os/ — remove old and copy fresh
|
||||
// Step 2: Detect which optional components the target has, so the
|
||||
// update refreshes those without re-adding deselected ones.
|
||||
components_set_from_target(drive_num);
|
||||
|
||||
// Step 3: Update os/ — overwrite existing and add new
|
||||
add_log("Updating os/...");
|
||||
flush_ui();
|
||||
|
||||
@@ -724,7 +779,7 @@ void do_update() {
|
||||
flush_ui(); st.step = STEP_ERROR; return;
|
||||
}
|
||||
|
||||
// Step 3: Update apps/ — copy all, overwriting existing and adding new
|
||||
// Step 4: Update apps/ — copy all, overwriting existing and adding new
|
||||
add_log("Updating apps/...");
|
||||
flush_ui();
|
||||
|
||||
@@ -736,16 +791,18 @@ void do_update() {
|
||||
flush_ui(); st.step = STEP_ERROR; return;
|
||||
}
|
||||
|
||||
// Step 4: Update lib/ — compiler runtime, headers, libc
|
||||
add_log("Updating lib/...");
|
||||
flush_ui();
|
||||
// Step 5: Update sdk/ — dev tools, headers, tcc, lua
|
||||
if (component_allows("sdk") || component_subtree_needed("sdk")) {
|
||||
add_log("Updating sdk/...");
|
||||
flush_ui();
|
||||
|
||||
snprintf(path_buf, sizeof(path_buf), "%d:/lib", drive_num);
|
||||
montauk::fmkdir(path_buf);
|
||||
snprintf(path_buf, sizeof(path_buf), "%d:/sdk", drive_num);
|
||||
montauk::fmkdir(path_buf);
|
||||
|
||||
if (!copy_recursive("0:/lib", path_buf)) {
|
||||
add_log("ERROR: Failed to update lib/");
|
||||
flush_ui(); st.step = STEP_ERROR; return;
|
||||
if (!copy_recursive("0:/sdk", path_buf)) {
|
||||
add_log("ERROR: Failed to update sdk/");
|
||||
flush_ui(); st.step = STEP_ERROR; return;
|
||||
}
|
||||
}
|
||||
|
||||
char copy_msg[64];
|
||||
|
||||
Reference in New Issue
Block a user