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:
2026-07-17 20:52:19 +02:00
co-authored by Claude Opus 4.8
parent 8e6b619b02
commit 0377be4524
18 changed files with 1647 additions and 813 deletions
+141 -84
View File
@@ -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];