nbos

nbos

https://git.tonybtw.com/nbos.git git://git.tonybtw.com/nbos.git

updated makefile, changed pkgs folder structure

Commit
f0ba53d642c4b275fd42c465b9f543c3594faf05
Parent
bca4fc3
Author
tonybanters <tonybanters@gmail.com>
Date
2026-05-10 16:43:39

Diff

diff --git a/.gitignore b/.gitignore
index 10e7215..8b0a7a8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,7 @@
 .clangd
 .cache/
 build/
+.build/
 notes/
 compile_commands.json
 nb
diff --git a/Makefile b/Makefile
index dcee192..debd4a6 100644
--- a/Makefile
+++ b/Makefile
@@ -1,5 +1,3 @@
-# Nb Makefile. C23, strict warnings, sanitizer-ready.
-
 CC      ?= cc
 CFLAGS  ?= -std=c23 -O2 -g \
            -Wall -Wextra -Wpedantic -Wshadow -Wconversion \
@@ -8,11 +6,15 @@ CFLAGS  ?= -std=c23 -O2 -g \
            -fno-strict-aliasing
 LDFLAGS ?=
 
+PROFILE ?= release
 ifdef SANITIZE
+    PROFILE := sanitize
     CFLAGS  += -fsanitize=address,undefined -fno-omit-frame-pointer
     LDFLAGS += -fsanitize=address,undefined
 endif
 
+BUILD_DIR := .build/$(PROFILE)
+
 INCLUDES = -Iinclude -Isrc
 
 SRC_FILES = \
@@ -31,20 +33,27 @@ SRC_FILES = \
     src/activate.c
 
 PKG_FILES = $(shell find pkgs -name '*.c')
-
 CONFIG_FILE ?= etc-example/config.c
 
-OBJ_FILES = $(SRC_FILES:.c=.o) $(PKG_FILES:.c=.o)
+ALL_SRCS  = $(SRC_FILES) $(PKG_FILES) $(CONFIG_FILE)
+OBJ_FILES = $(patsubst %.c,$(BUILD_DIR)/%.o,$(ALL_SRCS))
+DEP_FILES = $(OBJ_FILES:.o=.d)
 
 .PHONY: all clean registry switch
 
 all: nb
 
-nb: $(OBJ_FILES) $(CONFIG_FILE:.c=.o) pkgs/all.o
+nb: $(BUILD_DIR)/nb
+	@ln -sf $< $@
+
+$(BUILD_DIR)/nb: $(OBJ_FILES)
 	$(CC) $(LDFLAGS) -o $@ $^
 
-%.o: %.c
-	$(CC) $(CFLAGS) $(INCLUDES) -c -o $@ $<
+$(BUILD_DIR)/%.o: %.c
+	@mkdir -p $(@D)
+	$(CC) $(CFLAGS) $(INCLUDES) -MMD -MP -c -o $@ $<
+
+-include $(DEP_FILES)
 
 registry:
 	./tools/gen-registry.sh > pkgs/all.h.tmp
@@ -53,8 +62,7 @@ registry:
 	mv pkgs/all.c.tmp pkgs/all.c
 
 clean:
-	find . -name '*.o' -delete
-	rm -f nb
+	rm -rf .build nb
 
 switch: nb
 	./nb switch
diff --git a/etc-example/config.c b/etc-example/config.c
index fae9322..c41feb3 100644
--- a/etc-example/config.c
+++ b/etc-example/config.c
@@ -1,11 +1,11 @@
 /* /etc/nb/config.c */
 #include "../include/nbos.h"
-#include "../pkgs/core/glibc/glibc.h"
-#include "../pkgs/core/linux/linux.h"
-#include "../pkgs/editor/neovim/neovim.h"
-#include "../pkgs/wm/oxwm/oxwm.h"
-#include "../pkgs/shell/dash/dash.h"
-#include "../pkgs/browser/firefox/firefox.h"
+#include "../pkgs/glibc/glibc.h"
+#include "../pkgs/linux/linux.h"
+#include "../pkgs/neovim/neovim.h"
+#include "../pkgs/oxwm/oxwm.h"
+#include "../pkgs/dash/dash.h"
+#include "../pkgs/firefox/firefox.h"
 
 static const pkg *const my_pkgs[] = {
     &pkgs_glibc,
diff --git a/pkgs/shell/dash/build.sh b/pkgs/dash/build.sh
similarity index 100%
rename from pkgs/shell/dash/build.sh
rename to pkgs/dash/build.sh
diff --git a/pkgs/shell/dash/dash.c b/pkgs/dash/dash.c
similarity index 93%
rename from pkgs/shell/dash/dash.c
rename to pkgs/dash/dash.c
index 8f1689e..200f3f2 100644
--- a/pkgs/shell/dash/dash.c
+++ b/pkgs/dash/dash.c
@@ -1,5 +1,5 @@
 #include "dash.h"
-#include "../../core/glibc/glibc.h"
+#include "../glibc/glibc.h"
 
 static const pkg *const dash_deps[] = { &pkgs_glibc };
 
diff --git a/pkgs/shell/dash/dash.h b/pkgs/dash/dash.h
similarity index 68%
rename from pkgs/shell/dash/dash.h
rename to pkgs/dash/dash.h
index 9b94c2a..648e920 100644
--- a/pkgs/shell/dash/dash.h
+++ b/pkgs/dash/dash.h
@@ -1,5 +1,5 @@
 #ifndef PKGS_DASH_H
 #define PKGS_DASH_H
-#include "../../../include/nbos.h"
+#include "../../include/nbos.h"
 extern const pkg pkgs_dash;
 #endif
diff --git a/pkgs/browser/firefox/build.sh b/pkgs/firefox/build.sh
similarity index 100%
rename from pkgs/browser/firefox/build.sh
rename to pkgs/firefox/build.sh
diff --git a/pkgs/browser/firefox/firefox.c b/pkgs/firefox/firefox.c
similarity index 93%
rename from pkgs/browser/firefox/firefox.c
rename to pkgs/firefox/firefox.c
index cad79a3..dcc51f1 100644
--- a/pkgs/browser/firefox/firefox.c
+++ b/pkgs/firefox/firefox.c
@@ -1,5 +1,5 @@
 #include "firefox.h"
-#include "../../core/glibc/glibc.h"
+#include "../glibc/glibc.h"
 
 static const pkg *const firefox_deps[] = { &pkgs_glibc };
 
diff --git a/pkgs/browser/firefox/firefox.h b/pkgs/firefox/firefox.h
similarity index 70%
rename from pkgs/browser/firefox/firefox.h
rename to pkgs/firefox/firefox.h
index 7c2676c..b5975f1 100644
--- a/pkgs/browser/firefox/firefox.h
+++ b/pkgs/firefox/firefox.h
@@ -1,5 +1,5 @@
 #ifndef PKGS_FIREFOX_H
 #define PKGS_FIREFOX_H
-#include "../../../include/nbos.h"
+#include "../../include/nbos.h"
 extern const pkg pkgs_firefox;
 #endif
diff --git a/pkgs/core/glibc/build.sh b/pkgs/glibc/build.sh
similarity index 100%
rename from pkgs/core/glibc/build.sh
rename to pkgs/glibc/build.sh
diff --git a/pkgs/core/glibc/glibc.c b/pkgs/glibc/glibc.c
similarity index 100%
rename from pkgs/core/glibc/glibc.c
rename to pkgs/glibc/glibc.c
diff --git a/pkgs/core/glibc/glibc.h b/pkgs/glibc/glibc.h
similarity index 69%
rename from pkgs/core/glibc/glibc.h
rename to pkgs/glibc/glibc.h
index c0eab6a..fd41f35 100644
--- a/pkgs/core/glibc/glibc.h
+++ b/pkgs/glibc/glibc.h
@@ -1,5 +1,5 @@
 #ifndef PKGS_GLIBC_H
 #define PKGS_GLIBC_H
-#include "../../../include/nbos.h"
+#include "../../include/nbos.h"
 extern const pkg pkgs_glibc;
 #endif
diff --git a/pkgs/core/linux/build.sh b/pkgs/linux/build.sh
similarity index 100%
rename from pkgs/core/linux/build.sh
rename to pkgs/linux/build.sh
diff --git a/pkgs/core/linux/linux.c b/pkgs/linux/linux.c
similarity index 100%
rename from pkgs/core/linux/linux.c
rename to pkgs/linux/linux.c
diff --git a/pkgs/core/linux/linux.h b/pkgs/linux/linux.h
similarity index 69%
rename from pkgs/core/linux/linux.h
rename to pkgs/linux/linux.h
index 5295fc6..a7334b8 100644
--- a/pkgs/core/linux/linux.h
+++ b/pkgs/linux/linux.h
@@ -1,5 +1,5 @@
 #ifndef PKGS_LINUX_H
 #define PKGS_LINUX_H
-#include "../../../include/nbos.h"
+#include "../../include/nbos.h"
 extern const pkg pkgs_linux;
 #endif
diff --git a/pkgs/lang/lua/build.sh b/pkgs/lua/build.sh
similarity index 100%
rename from pkgs/lang/lua/build.sh
rename to pkgs/lua/build.sh
diff --git a/pkgs/lang/lua/lua.c b/pkgs/lua/lua.c
similarity index 92%
rename from pkgs/lang/lua/lua.c
rename to pkgs/lua/lua.c
index 219a78b..cc1e122 100644
--- a/pkgs/lang/lua/lua.c
+++ b/pkgs/lua/lua.c
@@ -1,5 +1,5 @@
 #include "lua.h"
-#include "../../core/glibc/glibc.h"
+#include "../glibc/glibc.h"
 
 static const pkg *const lua_deps[] = { &pkgs_glibc };
 
diff --git a/pkgs/lang/lua/lua.h b/pkgs/lua/lua.h
similarity index 67%
rename from pkgs/lang/lua/lua.h
rename to pkgs/lua/lua.h
index e82370d..8d95525 100644
--- a/pkgs/lang/lua/lua.h
+++ b/pkgs/lua/lua.h
@@ -1,5 +1,5 @@
 #ifndef PKGS_LUA_H
 #define PKGS_LUA_H
-#include "../../../include/nbos.h"
+#include "../../include/nbos.h"
 extern const pkg pkgs_lua;
 #endif
diff --git a/pkgs/editor/neovim/build.sh b/pkgs/neovim/build.sh
similarity index 100%
rename from pkgs/editor/neovim/build.sh
rename to pkgs/neovim/build.sh
diff --git a/pkgs/editor/neovim/neovim.c b/pkgs/neovim/neovim.c
similarity index 88%
rename from pkgs/editor/neovim/neovim.c
rename to pkgs/neovim/neovim.c
index dae3833..bf27b0e 100644
--- a/pkgs/editor/neovim/neovim.c
+++ b/pkgs/neovim/neovim.c
@@ -1,6 +1,6 @@
 #include "neovim.h"
-#include "../../core/glibc/glibc.h"
-#include "../../lang/lua/lua.h"
+#include "../glibc/glibc.h"
+#include "../lua/lua.h"
 
 static const pkg *const neovim_deps[] = {
     &pkgs_glibc,
diff --git a/pkgs/editor/neovim/neovim.h b/pkgs/neovim/neovim.h
similarity index 69%
rename from pkgs/editor/neovim/neovim.h
rename to pkgs/neovim/neovim.h
index c78de21..8a32cd0 100644
--- a/pkgs/editor/neovim/neovim.h
+++ b/pkgs/neovim/neovim.h
@@ -1,5 +1,5 @@
 #ifndef PKGS_NEOVIM_H
 #define PKGS_NEOVIM_H
-#include "../../../include/nbos.h"
+#include "../../include/nbos.h"
 extern const pkg pkgs_neovim;
 #endif
diff --git a/pkgs/wm/oxwm/build.sh b/pkgs/oxwm/build.sh
similarity index 100%
rename from pkgs/wm/oxwm/build.sh
rename to pkgs/oxwm/build.sh
diff --git a/pkgs/wm/oxwm/oxwm.c b/pkgs/oxwm/oxwm.c
similarity index 88%
rename from pkgs/wm/oxwm/oxwm.c
rename to pkgs/oxwm/oxwm.c
index aadb31c..f231af1 100644
--- a/pkgs/wm/oxwm/oxwm.c
+++ b/pkgs/oxwm/oxwm.c
@@ -1,6 +1,6 @@
 #include "oxwm.h"
-#include "../../core/glibc/glibc.h"
-#include "../../lang/lua/lua.h"
+#include "../glibc/glibc.h"
+#include "../lua/lua.h"
 
 static const pkg *const oxwm_deps[] = {
     &pkgs_glibc,
diff --git a/pkgs/wm/oxwm/oxwm.h b/pkgs/oxwm/oxwm.h
similarity index 68%
rename from pkgs/wm/oxwm/oxwm.h
rename to pkgs/oxwm/oxwm.h
index b83b879..02dbe7a 100644
--- a/pkgs/wm/oxwm/oxwm.h
+++ b/pkgs/oxwm/oxwm.h
@@ -1,5 +1,5 @@
 #ifndef PKGS_OXWM_H
 #define PKGS_OXWM_H
-#include "../../../include/nbos.h"
+#include "../../include/nbos.h"
 extern const pkg pkgs_oxwm;
 #endif
diff --git a/src/build.h b/src/build.h
index 2bfd43d..44f439e 100644
--- a/src/build.h
+++ b/src/build.h
@@ -11,6 +11,7 @@
     const pkg          *p,
     const pkg_refs     *all_pkgs,
     const resolved     *resolved_pkgs,
-    size_t              pkg_idx);
+    size_t              pkg_idx
+);
 
 #endif
diff --git a/src/error.c b/src/error.c
index 95e0494..b22f278 100644
--- a/src/error.c
+++ b/src/error.c
@@ -16,6 +16,9 @@ void resolve_error_print(const resolve_error *e) {
         case RESOLVE_E_DUPLICATE_NAME:
             fprintf(stderr, "nb: duplicate package name '%s' (two recipes export the same name)\n", e->pkg_name);
             return;
+        case RESOLVE_E_OOM:
+            fprintf(stderr, "nb: out of memory while resolving packages\n");
+            return;
     }
 }
 
diff --git a/src/error.h b/src/error.h
index 18fa73a..795936e 100644
--- a/src/error.h
+++ b/src/error.h
@@ -9,6 +9,7 @@ typedef enum : uint8_t {
     RESOLVE_E_CYCLE,
     RESOLVE_E_MISSING_DEP,
     RESOLVE_E_DUPLICATE_NAME,
+    RESOLVE_E_OOM,
 } resolve_error_kind;
 
 typedef struct {
diff --git a/src/fetch.c b/src/fetch.c
index 9ab7bdb..4243142 100644
--- a/src/fetch.c
+++ b/src/fetch.c
@@ -19,6 +19,7 @@
  * Return: FETCH_OK_VAL on success, a tagged error otherwise.
  */
 fetch_error fetch(const pkg *p, const char *dest_path) {
+    // TODO: replace this with execvpe or libcurl
     char *const argv[] = {
         "/usr/bin/curl",
         "-fsSL",
diff --git a/src/main.c b/src/main.c
index a97cc03..6dcd071 100644
--- a/src/main.c
+++ b/src/main.c
@@ -110,7 +110,8 @@ static void usage(void) {
         "  validate   check the current config without activating\n"
         "  rollback   activate the previous generation\n"
         "  diff       diff two generations (default: previous vs current)\n"
-        "  gc         remove store paths not referenced by any generation\n");
+        "  gc         remove store paths not referenced by any generation\n"
+    );
 }
 
 int main(int argc, char **argv) {
diff --git a/src/realize.c b/src/realize.c
index 4aa3b01..ece3aa3 100644
--- a/src/realize.c
+++ b/src/realize.c
@@ -15,14 +15,16 @@
 realize_error realize(
     arena              *a,
     const pkg_refs     *all_pkgs,
-    const resolved_list *resolved)
-{
+    const resolved_list *resolved
+) {
     for (size_t i = 0; i < resolved->len; i++) {
-        realize_error err = build_pkg(a,
-                                       resolved->data[i].def,
-                                       all_pkgs,
-                                       resolved->data,
-                                       i);
+        realize_error err = build_pkg(
+            a,
+            resolved->data[i].def,
+            all_pkgs,
+            resolved->data,
+            i
+        );
         if (err.kind != REALIZE_OK) return err;
     }
     return REALIZE_OK_VAL;
diff --git a/src/resolve.c b/src/resolve.c
index 74625ae..e07ea8a 100644
--- a/src/resolve.c
+++ b/src/resolve.c
@@ -4,7 +4,6 @@
 #include <stdalign.h>
 #include <string.h>
 
-#include "hash.h"
 #include "store.h"
 
 typedef enum : uint8_t {
@@ -13,63 +12,95 @@ typedef enum : uint8_t {
     VISITED,
 } visit_state;
 
-static size_t find_pkg(const pkg_refs *pkgs, const pkg *needle) {
-    for (size_t i = 0; i < pkgs->len; i++) {
-        if (pkgs->data[i] == needle) return i;
+typedef struct closure_node closure_node;
+struct closure_node {
+    const pkg    *p;
+    closure_node *next;
+};
+
+static bool closure_contains(const closure_node *head, const pkg *p) {
+    for (const closure_node *n = head; n != nullptr; n = n->next) {
+        if (n->p == p) return true;
+    }
+    return false;
+}
+
+static bool closure_collect(
+        arena         *a,
+        const pkg     *p,
+        closure_node **head,
+        size_t        *count
+    ) {
+    if (closure_contains(*head, p)) return true;
+
+    closure_node *node = arena_alloc(a, sizeof(closure_node), alignof(closure_node));
+    if (node == nullptr) return false;
+    node->p    = p;
+    node->next = *head;
+    *head      = node;
+    (*count)++;
+
+    for (size_t i = 0; i < p->deps.len; i++) {
+        if (!closure_collect(a, p->deps.data[i], head, count)) return false;
+    }
+    return true;
+}
+
+static size_t closure_index(
+    const pkg *const *closure,
+    size_t            n,
+    const pkg        *target)
+{
+    for (size_t i = 0; i < n; i++) {
+        if (closure[i] == target) return i;
     }
     return SIZE_MAX;
 }
 
 static resolve_error visit(
-    arena              *a,
-    const pkg_refs     *pkgs,
-    size_t              idx,
-    visit_state        *state,
-    resolved           *out)
+    arena             *a,
+    const pkg *const  *closure,
+    size_t             n_closure,
+    size_t             idx,
+    visit_state       *state,
+    resolved          *out,
+    size_t            *out_len)
 {
     switch (state[idx]) {
         case VISITED:  return RESOLVE_OK_VAL;
         case VISITING: return (resolve_error){
             .kind = RESOLVE_E_CYCLE,
-            .pkg_name = pkgs->data[idx]->name,
+            .pkg_name = closure[idx]->name,
         };
         case UNVISITED: break;
     }
 
     state[idx] = VISITING;
-    const pkg *p = pkgs->data[idx];
+    const pkg *p = closure[idx];
 
     for (size_t i = 0; i < p->deps.len; i++) {
-        const pkg *dep = p->deps.data[i];
-        size_t dep_idx = find_pkg(pkgs, dep);
-        if (dep_idx == SIZE_MAX) {
-            return (resolve_error){
-                .kind = RESOLVE_E_MISSING_DEP,
-                .pkg_name = p->name,
-                .dep_name = dep->name,
-            };
-        }
-
-        resolve_error err = visit(a, pkgs, dep_idx, state, out);
+        size_t dep_idx = closure_index(closure, n_closure, p->deps.data[i]);
+        resolve_error err = visit(a, closure, n_closure, dep_idx, state, out, out_len);
         if (err.kind != RESOLVE_OK) return err;
     }
 
-    out[idx].def = p;
-    out[idx].store_path = store_path_compute(a, p, pkgs, out);
-
+    out[*out_len].def        = p;
+    out[*out_len].store_path = store_path_compute(a, p, out, *out_len);
+    (*out_len)++;
     state[idx] = VISITED;
     return RESOLVE_OK_VAL;
 }
 
 /**
- * resolve() - Topologically sort packages and compute their store paths.
- * @a: Arena holding the resulting visit-state and resolved arrays.
- * @cfg: System configuration providing the package list.
+ * resolve() - Compute the build closure and topologically sort it.
+ * @a: Arena holding the resulting closure, visit-state, and resolved arrays.
+ * @cfg: System configuration providing the package roots.
  * @out: Filled with topologically-sorted &resolved entries on success.
  *
- * Rejects duplicate package names up front, then performs a DFS that
- * detects cycles and missing deps. Each entry's store path depends on
- * its deps' store paths, so children must be resolved first.
+ * Roots are everything the config references directly: @cfg->pkgs entries,
+ * @cfg->boot.kernel, and each @cfg->users[i].shell. Transitive deps are
+ * auto-discovered into the closure. The output is in post-order so
+ * &realize can iterate it directly.
  *
  * Return: RESOLVE_OK_VAL on success, a tagged error otherwise.
  */
@@ -89,20 +120,51 @@ resolve_error resolve(
         }
     }
 
-    visit_state *state = arena_alloc(a, cfg->pkgs.len * sizeof(visit_state),
-                                     alignof(visit_state));
-    resolved    *res   = arena_alloc(a, cfg->pkgs.len * sizeof(resolved),
-                                     alignof(resolved));
-    if (state == nullptr || res == nullptr) {
-        return (resolve_error){ .kind = RESOLVE_E_MISSING_DEP };
+    closure_node *head = nullptr;
+    size_t n_closure = 0;
+    for (size_t i = 0; i < cfg->pkgs.len; i++) {
+        if (!closure_collect(a, cfg->pkgs.data[i], &head, &n_closure)) {
+            return (resolve_error){ .kind = RESOLVE_E_OOM };
+        }
+    }
+    if (cfg->boot.kernel != nullptr) {
+        if (!closure_collect(a, cfg->boot.kernel, &head, &n_closure)) {
+            return (resolve_error){ .kind = RESOLVE_E_OOM };
+        }
+    }
+    for (size_t i = 0; i < cfg->users.len; i++) {
+        const pkg *shell = cfg->users.data[i].shell;
+        if (shell != nullptr) {
+            if (!closure_collect(a, shell, &head, &n_closure)) {
+                return (resolve_error){ .kind = RESOLVE_E_OOM };
+            }
+        }
+    }
+
+    const pkg **closure = arena_alloc(a, n_closure * sizeof(const pkg *),
+                                       alignof(const pkg *));
+    visit_state *state  = arena_alloc(a, n_closure * sizeof(visit_state),
+                                       alignof(visit_state));
+    resolved    *res    = arena_alloc(a, n_closure * sizeof(resolved),
+                                       alignof(resolved));
+    if (closure == nullptr || state == nullptr || res == nullptr) {
+        return (resolve_error){ .kind = RESOLVE_E_OOM };
     }
 
-    for (size_t i = 0; i < cfg->pkgs.len; i++) {
-        resolve_error err = visit(a, &cfg->pkgs, i, state, res);
+    {
+        size_t i = 0;
+        for (closure_node *n = head; n != nullptr; n = n->next) {
+            closure[i++] = n->p;
+        }
+    }
+
+    size_t res_len = 0;
+    for (size_t i = 0; i < n_closure; i++) {
+        resolve_error err = visit(a, closure, n_closure, i, state, res, &res_len);
         if (err.kind != RESOLVE_OK) return err;
     }
 
     out->data = res;
-    out->len  = cfg->pkgs.len;
+    out->len  = res_len;
     return RESOLVE_OK_VAL;
 }
diff --git a/src/sandbox.c b/src/sandbox.c
index 8acbfdb..173a83d 100644
--- a/src/sandbox.c
+++ b/src/sandbox.c
@@ -28,8 +28,8 @@ int sandbox_setup(
     const char     *sandbox_root,
     const pkg_refs *deps,
     const resolved *resolved_deps,
-    const char     *src_dir)
-{
+    const char     *src_dir
+) {
     (void)sandbox_root;
     (void)deps;
     (void)resolved_deps;
diff --git a/src/store.c b/src/store.c
index eeb4b9d..e621653 100644
--- a/src/store.c
+++ b/src/store.c
@@ -34,26 +34,16 @@ static void base32_encode(const uint8_t *in, size_t len, char *out) {
     out[out_idx] = '\0';
 }
 
-static const char *resolved_path_for(
-    const pkg          *target,
-    const pkg_refs     *all_pkgs,
-    const resolved     *resolved_pkgs)
-{
-    for (size_t i = 0; i < all_pkgs->len; i++) {
-        if (all_pkgs->data[i] == target) {
-            return resolved_pkgs[i].store_path;
-        }
-    }
-    return nullptr;
-}
+
+
 
 /**
  * store_path_compute() - Compute the content-addressed store path for @p.
  * @a: Arena that owns the returned string.
  * @p: Package whose inputs determine the path.
- * @all_pkgs: Full package list, used to look up dep store paths.
- * @resolved_pkgs: Parallel array of resolved entries; deps must already
+ * @resolved_pkgs: Resolved entries already topo-sorted; deps must already
  *                 have @store_path populated.
+ * @n_resolved: Number of valid entries in @resolved_pkgs.
  *
  * Hashes a length-prefixed canonical encoding of the package inputs
  * (name, version, src URL, source sha256, build flags, build system,
@@ -66,8 +56,8 @@ static const char *resolved_path_for(
 const char *store_path_compute(
     arena              *a,
     const pkg          *p,
-    const pkg_refs     *all_pkgs,
-    const resolved     *resolved_pkgs)
+    const resolved     *resolved_pkgs,
+    size_t              n_resolved)
 {
     sha256_ctx ctx;
     sha256_init(&ctx);
@@ -90,9 +80,14 @@ const char *store_path_compute(
     sha256_update(&ctx, "\0", 1);
 
     for (size_t i = 0; i < p->deps.len; i++) {
-        const char *dep_path = resolved_path_for(p->deps.data[i],
-                                                  all_pkgs,
-                                                  resolved_pkgs);
+        const pkg *target = p->deps.data[i];
+        const char *dep_path = nullptr;
+        for (size_t j = 0; j < n_resolved; j++) {
+            if (resolved_pkgs[j].def == target) {
+                dep_path = resolved_pkgs[j].store_path;
+                break;
+            }
+        }
         if (dep_path == nullptr) return nullptr;
         HASH_FIELD(dep_path);
     }
diff --git a/src/store.h b/src/store.h
index 3b396ec..dde1b17 100644
--- a/src/store.h
+++ b/src/store.h
@@ -10,8 +10,8 @@
 [[nodiscard]] const char *store_path_compute(
     arena              *a,
     const pkg          *p,
-    const pkg_refs     *all_pkgs,
-    const resolved     *resolved_pkgs);
+    const resolved     *resolved_pkgs,
+    size_t              n_resolved);
 
 [[nodiscard]] bool store_path_exists(const char *store_path);
 
diff --git a/src/validate.c b/src/validate.c
index 04548de..3e519dd 100644
--- a/src/validate.c
+++ b/src/validate.c
@@ -3,13 +3,6 @@
 #include <stdio.h>
 #include <string.h>
 
-static bool pkg_in_list(const pkg *p, const pkg_refs *list) {
-    for (size_t i = 0; i < list->len; i++) {
-        if (list->data[i] == p) return true;
-    }
-    return false;
-}
-
 static bool is_known_bootloader(const char *name) {
     return strcmp(name, "limine")   == 0
         || strcmp(name, "grub")     == 0
@@ -21,8 +14,11 @@ static bool is_known_bootloader(const char *name) {
  * @cfg: Configuration to inspect.
  *
  * Covers what resolve() does not: hostname presence, bootloader
- * recognition, kernel/shell pointers belong to the package list, and
- * uniqueness of user and service names. Errors are printed to stderr.
+ * recognition, non-null kernel/shell pointers, and uniqueness of user
+ * and service names. Pkg references (kernel, shells, declared pkgs)
+ * are all closure roots — resolve() auto-discovers transitive deps,
+ * so no "is in the package list" check is needed. Errors are printed
+ * to stderr.
  *
  * Return: true if every check passes.
  */
@@ -42,10 +38,6 @@ bool validate(const system_cfg *cfg) {
     if (cfg->boot.kernel == nullptr) {
         fprintf(stderr, "nb: boot.kernel is null\n");
         ok = false;
-    } else if (!pkg_in_list(cfg->boot.kernel, &cfg->pkgs)) {
-        fprintf(stderr, "nb: boot.kernel '%s' is not in the package list\n",
-                cfg->boot.kernel->name);
-        ok = false;
     }
 
     for (size_t i = 0; i < cfg->users.len; i++) {
@@ -60,10 +52,6 @@ bool validate(const system_cfg *cfg) {
         if (u->shell == nullptr) {
             fprintf(stderr, "nb: user '%s' has null shell\n", u->name);
             ok = false;
-        } else if (!pkg_in_list(u->shell, &cfg->pkgs)) {
-            fprintf(stderr, "nb: user '%s' shell '%s' is not in the package list\n",
-                    u->name, u->shell->name);
-            ok = false;
         }
 
         for (size_t j = i + 1; j < cfg->users.len; j++) {
diff --git a/tools/gen-registry.sh b/tools/gen-registry.sh
index 26b68fe..1e7a545 100755
--- a/tools/gen-registry.sh
+++ b/tools/gen-registry.sh
@@ -10,7 +10,7 @@ set -eu
 PKGS_DIR="${PKGS_DIR:-pkgs}"
 MODE="${1:-h}"
 
-recipes=$(find "$PKGS_DIR" -mindepth 3 -maxdepth 3 -name '*.h' \
+recipes=$(find "$PKGS_DIR" -mindepth 2 -maxdepth 2 -name '*.h' \
             ! -name 'all.h' | sort)
 
 if [ "$MODE" = "h" ] || [ "$MODE" = "--h" ]; then