dep

Package manager for embedded C libraries
git clone git://git.finwo.net/app/dep
Log | Files | Refs | README | LICENSE

main.c (21466B)


      1 #ifndef _GNU_SOURCE
      2 #define _GNU_SOURCE
      3 #endif
      4 #include <errno.h>
      5 #include <fcntl.h>
      6 #include <limits.h>
      7 #include <stdio.h>
      8 #include <stdlib.h>
      9 #include <string.h>
     10 #include <sys/stat.h>
     11 #include <sys/types.h>
     12 #include <sys/wait.h>
     13 #include <unistd.h>
     14 
     15 #ifdef __APPLE__
     16 #include <crt_externs.h>
     17 #define environ (*_NSGetEnviron())
     18 #endif
     19 
     20 #include "command/command.h"
     21 #include "common/github-utils.h"
     22 #include "common/net-utils.h"
     23 #include "cozis/tinytemplate.h"
     24 #include "emmanuel-marty/em_inflate.h"
     25 #include "erkkah/naett.h"
     26 #include "rxi/microtar.h"
     27 
     28 /* Forward declarations */
     29 static int install_dependency(const char *name, const char *spec);
     30 static int process_dep_config_mk(const char *name, FILE *dst_config);
     31 static int process_dep_for_config(const char *name, FILE *dst_config);
     32 
     33 /* Config-phase visited dependency tracking (used only in Pass 2) */
     34 static char **config_visited_deps          = NULL;
     35 static int    config_visited_deps_count    = 0;
     36 static int    config_visited_deps_capacity = 0;
     37 
     38 static int config_is_visited(const char *name) {
     39   for (int i = 0; i < config_visited_deps_count; i++) {
     40     if (strcmp(config_visited_deps[i], name) == 0) {
     41       return 1;
     42     }
     43   }
     44   return 0;
     45 }
     46 
     47 static int config_mark_visited(const char *name) {
     48   if (config_is_visited(name)) {
     49     return 0;
     50   }
     51 
     52   if (config_visited_deps_count >= config_visited_deps_capacity) {
     53     int    new_capacity = config_visited_deps_capacity == 0 ? 16 : config_visited_deps_capacity * 2;
     54     char **new_deps     = realloc(config_visited_deps, new_capacity * sizeof(char *));
     55     if (!new_deps) {
     56       fprintf(stderr, "Error: failed to allocate memory for config visited deps\n");
     57       return -1;
     58     }
     59     config_visited_deps          = new_deps;
     60     config_visited_deps_capacity = new_capacity;
     61   }
     62 
     63   config_visited_deps[config_visited_deps_count] = strdup(name);
     64   if (!config_visited_deps[config_visited_deps_count]) {
     65     fprintf(stderr, "Error: failed to allocate memory for config dep name\n");
     66     return -1;
     67   }
     68   config_visited_deps_count++;
     69   return 1;
     70 }
     71 
     72 static void config_clear_visited(void) {
     73   for (int i = 0; i < config_visited_deps_count; i++) {
     74     free(config_visited_deps[i]);
     75   }
     76   free(config_visited_deps);
     77   config_visited_deps          = NULL;
     78   config_visited_deps_count    = 0;
     79   config_visited_deps_capacity = 0;
     80 }
     81 
     82 static int dir_exists(const char *path) {
     83   struct stat st;
     84   return stat(path, &st) == 0 && S_ISDIR(st.st_mode);
     85 }
     86 
     87 static int mkdir_recursive(const char *path) {
     88   char   tmp[PATH_MAX];
     89   char  *p = NULL;
     90   size_t len;
     91 
     92   snprintf(tmp, sizeof(tmp), "%s", path);
     93   len = strlen(tmp);
     94   if (tmp[len - 1] == '/') {
     95     tmp[len - 1] = '\0';
     96   }
     97 
     98   for (p = tmp + 1; *p; p++) {
     99     if (*p == '/') {
    100       *p = '\0';
    101       mkdir(tmp, 0755);
    102       *p = '/';
    103     }
    104   }
    105   return mkdir(tmp, 0755);
    106 }
    107 
    108 static char *trim_whitespace(char *str) {
    109   while (*str == ' ' || *str == '\t' || *str == '\n' || *str == '\r') str++;
    110   if (*str == '\0') return str;
    111   char *end = str + strlen(str) - 1;
    112   while (end > str && (*end == ' ' || *end == '\t' || *end == '\n' || *end == '\r')) {
    113     *end = '\0';
    114     end--;
    115   }
    116   return str;
    117 }
    118 
    119 static char *spec_to_url(const char *name, const char *spec) {
    120   if (strlen(spec) > 0 && is_url(spec)) {
    121     return strdup(spec);
    122   }
    123 
    124   char *full_ref = NULL;
    125 
    126   if (strlen(spec) > 0) {
    127     full_ref = github_matching_ref(name, spec);
    128     if (!full_ref) {
    129       fprintf(stderr, "Error: ref '%s' not found for %s\n", spec, name);
    130       return NULL;
    131     }
    132   } else {
    133     char *branch = github_default_branch(name);
    134     if (!branch) {
    135       fprintf(stderr, "Warning: could not determine default branch for %s, using 'main'\n", name);
    136       branch = strdup("main");
    137     }
    138     full_ref = malloc(256);
    139     if (full_ref) {
    140       snprintf(full_ref, 256, "refs/heads/%s", branch);
    141     }
    142     free(branch);
    143   }
    144 
    145   if (!full_ref) {
    146     fprintf(stderr, "Error: could not determine ref for %s\n", name);
    147     return NULL;
    148   }
    149 
    150   char *url = malloc(2048);
    151   if (!url) {
    152     free(full_ref);
    153     return NULL;
    154   }
    155   snprintf(url, 2048, "https://github.com/%s/archive/%s.tar.gz", name, full_ref);
    156   free(full_ref);
    157   return url;
    158 }
    159 
    160 static int process_dep_export_file(const char *dep_dir, const char *name) {
    161   char export_path[PATH_MAX];
    162   snprintf(export_path, sizeof(export_path), "%s/.dep.export", dep_dir);
    163   FILE *f = fopen(export_path, "r");
    164   if (!f) {
    165     return 0;
    166   }
    167 
    168   char dep_base[PATH_MAX];
    169   if (getcwd(dep_base, sizeof(dep_base)) == NULL) {
    170     fprintf(stderr, "Error: failed to get current working directory\n");
    171     fclose(f);
    172     return -1;
    173   }
    174 
    175   char line[PATH_MAX];
    176   while (fgets(line, sizeof(line), f)) {
    177     char *comment = strchr(line, '#');
    178     if (comment) *comment = '\0';
    179 
    180     char *trimmed = trim_whitespace(line);
    181     if (strlen(trimmed) == 0) continue;
    182 
    183     char source[512] = {0};
    184     char target[512] = {0};
    185 
    186     char *space_pos        = strchr(trimmed, ' ');
    187     char *tab_pos          = strchr(trimmed, '\t');
    188     char *first_whitespace = NULL;
    189     if (space_pos && tab_pos) {
    190       first_whitespace = (space_pos < tab_pos) ? space_pos : tab_pos;
    191     } else if (space_pos) {
    192       first_whitespace = space_pos;
    193     } else if (tab_pos) {
    194       first_whitespace = tab_pos;
    195     }
    196 
    197     if (first_whitespace) {
    198       size_t source_len = first_whitespace - trimmed;
    199       strncpy(source, trimmed, source_len);
    200       source[source_len] = '\0';
    201       strcpy(target, trim_whitespace(first_whitespace + 1));
    202     } else {
    203       continue;
    204     }
    205 
    206     if (strlen(source) == 0 || strlen(target) == 0) continue;
    207 
    208     char source_parent[PATH_MAX];
    209     strncpy(source_parent, source, sizeof(source_parent) - 1);
    210     source_parent[sizeof(source_parent) - 1] = '\0';
    211     char *last_slash                         = strrchr(source_parent, '/');
    212     if (last_slash) {
    213       *last_slash = '\0';
    214       char parent_dir[PATH_MAX];
    215       snprintf(parent_dir, sizeof(parent_dir), "lib/.dep/%s", source_parent);
    216       mkdir_recursive(parent_dir);
    217     } else {
    218       mkdir_recursive("lib/.dep");
    219     }
    220 
    221     char target_abs[PATH_MAX];
    222     snprintf(target_abs, sizeof(target_abs), "%s/lib/%s/%s", dep_base, name, target);
    223 
    224     char link_path[PATH_MAX];
    225     snprintf(link_path, sizeof(link_path), "lib/.dep/%s", source);
    226 
    227     unlink(link_path);
    228     if (symlink(target_abs, link_path) != 0) {
    229       fprintf(stderr, "Warning: failed to create symlink %s -> %s\n", link_path, target_abs);
    230     } else {
    231       printf("Exported %s -> %s\n", source, target_abs);
    232     }
    233   }
    234 
    235   fclose(f);
    236   return 0;
    237 }
    238 
    239 static int process_dep_file_in_dir(const char *dep_dir);
    240 static int execute_postinstall_hook(const char *dep_dir);
    241 
    242 static int process_dep_file_in_dir(const char *dep_dir) {
    243   char dep_path[PATH_MAX];
    244   snprintf(dep_path, sizeof(dep_path), "%s/.dep", dep_dir);
    245   FILE *f = fopen(dep_path, "r");
    246   if (!f) {
    247     // No .dep file is not an error
    248     return 0;
    249   }
    250 
    251   char line[LINE_MAX];
    252   while (fgets(line, sizeof(line), f)) {
    253     char *comment = strchr(line, '#');
    254     if (comment) *comment = '\0';
    255 
    256     char *trimmed = trim_whitespace(line);
    257     if (strlen(trimmed) == 0) continue;
    258 
    259     char name[256]  = {0};
    260     char spec[1024] = {0};
    261 
    262     char *space_pos        = strchr(trimmed, ' ');
    263     char *tab_pos          = strchr(trimmed, '\t');
    264     char *first_whitespace = NULL;
    265     if (space_pos && tab_pos) {
    266       first_whitespace = (space_pos < tab_pos) ? space_pos : tab_pos;
    267     } else if (space_pos) {
    268       first_whitespace = space_pos;
    269     } else if (tab_pos) {
    270       first_whitespace = tab_pos;
    271     }
    272 
    273     if (first_whitespace) {
    274       size_t name_len = first_whitespace - trimmed;
    275       strncpy(name, trimmed, name_len);
    276       name[name_len] = '\0';
    277       strcpy(spec, trim_whitespace(first_whitespace + 1));
    278     } else {
    279       strncpy(name, trimmed, sizeof(name) - 1);
    280       name[sizeof(name) - 1] = '\0';
    281     }
    282 
    283     if (strlen(name) == 0) continue;
    284 
    285     install_dependency(name, spec);
    286   }
    287 
    288   fclose(f);
    289   return 0;
    290 }
    291 
    292 static int execute_postinstall_hook(const char *dep_dir) {
    293   char cwd[PATH_MAX];
    294   if (getcwd(cwd, sizeof(cwd)) == NULL) {
    295     fprintf(stderr, "Error: failed to get current working directory\n");
    296     return -1;
    297   }
    298 
    299   if (chdir(dep_dir) != 0) {
    300     fprintf(stderr, "Error: failed to change to dependency directory\n");
    301     return -1;
    302   }
    303 
    304   char hook_path[PATH_MAX];
    305   snprintf(hook_path, sizeof(hook_path), "./.dep.hook.postinstall");
    306 
    307   struct stat st;
    308   if (stat(hook_path, &st) != 0) {
    309     chdir(cwd);
    310     return 0;
    311   }
    312 
    313   int exec_bits = S_IXUSR | S_IXGRP | S_IXOTH;
    314   if (!(st.st_mode & exec_bits)) {
    315     fprintf(stderr, "Warning: %s is not executable, making executable...\n", hook_path);
    316     if (chmod(hook_path, st.st_mode | exec_bits) != 0) {
    317       fprintf(stderr, "Error: failed to make hook executable\n");
    318       chdir(cwd);
    319       return -1;
    320     }
    321     if (stat(hook_path, &st) != 0) {
    322       chdir(cwd);
    323       return 0;
    324     }
    325   }
    326 
    327   pid_t pid = fork();
    328   if (pid == 0) {
    329     char *const argv[] = {hook_path, NULL};
    330     execve(hook_path, argv, environ);
    331     fprintf(stderr, "Error: execve failed: errno=%d\n", errno);
    332     _exit(127);
    333   } else if (pid < 0) {
    334     chdir(cwd);
    335     fprintf(stderr, "Error: failed to fork for postinstall hook\n");
    336     return -1;
    337   }
    338 
    339   int status;
    340   waitpid(pid, &status, 0);
    341 
    342   chdir(cwd);
    343 
    344   if (WIFEXITED(status) && WEXITSTATUS(status) == 0) {
    345     printf("Executed postinstall hook for %s\n", dep_dir);
    346     return 0;
    347   } else {
    348     fprintf(stderr, "Error: postinstall hook failed with exit code %d\n", WIFEXITED(status) ? WEXITSTATUS(status) : -1);
    349     return -1;
    350   }
    351 }
    352 
    353 static int install_dependency(const char *name, const char *spec);
    354 
    355 struct template_ctx {
    356   const char *lib_path;
    357   FILE       *fp;
    358 };
    359 
    360 static bool module_dict_getter(void *data, const char *name, size_t len, tinytemplate_value_t *value) {
    361   const char *lib_path = data;
    362   if (len == 7 && strncmp(name, "dirname", 7) == 0) {
    363     tinytemplate_set_string(value, lib_path, strlen(lib_path));
    364     return true;
    365   }
    366   char orig[256];
    367   int  n = snprintf(orig, sizeof(orig), "{%.*s}", (int)len, name);
    368   tinytemplate_set_string(value, orig, n);
    369   return true;
    370 }
    371 
    372 static bool template_getter(void *data, const char *name, size_t len, tinytemplate_value_t *value) {
    373   struct template_ctx *ctx = data;
    374 
    375   if (len == 6 && strncmp(name, "module", 6) == 0) {
    376     tinytemplate_set_dict(value, (void *)ctx->lib_path, module_dict_getter);
    377     return true;
    378   }
    379 
    380   char orig[256];
    381   int  n = snprintf(orig, sizeof(orig), "{{%.*s}}", (int)len, name);
    382   tinytemplate_set_string(value, orig, n);
    383   return true;
    384 }
    385 
    386 static void template_callback(void *data, const char *str, size_t len) {
    387   struct template_ctx *ctx = data;
    388   fwrite(str, 1, len, ctx->fp);
    389 }
    390 
    391 static int process_dep_config_mk(const char *name, FILE *dst_config) {
    392   char lib_path[PATH_MAX];
    393   snprintf(lib_path, sizeof(lib_path), "lib/%s", name);
    394 
    395   char src_config_path[PATH_MAX];
    396   // Try export.mk first (new preferred name), fall back to config.mk
    397   snprintf(src_config_path, sizeof(src_config_path), "%s/export.mk", lib_path);
    398 
    399   FILE *dep_config   = fopen(src_config_path, "r");
    400   int   using_legacy = 0;
    401   if (!dep_config) {
    402     // Fall back to legacy config.mk
    403     snprintf(src_config_path, sizeof(src_config_path), "%s/config.mk", lib_path);
    404     dep_config = fopen(src_config_path, "r");
    405     if (dep_config) {
    406       using_legacy = 1;
    407       fprintf(stderr, "Deprecation warning: %s uses config.mk, please rename to export.mk\n", name);
    408     } else {
    409       return 0;
    410     }
    411   }
    412 
    413   // Write dependency name as a comment
    414   fprintf(dst_config, "\n# %s\n", name);
    415 
    416   char line[LINE_MAX];
    417   while (fgets(line, sizeof(line), dep_config)) {
    418     size_t linelen = strlen(line);
    419     if (linelen > 0 && line[linelen - 1] == '\n') {
    420       line[--linelen] = '\0';
    421     }
    422     if (linelen > 0 && line[linelen - 1] == '\r') {
    423       line[--linelen] = '\0';
    424     }
    425 
    426     if (linelen == 0) {
    427       fputc('\n', dst_config);
    428       continue;
    429     }
    430 
    431     tinytemplate_instr_t program[256];
    432     size_t               num_instr = 0;
    433     char                 errmsg[256];
    434 
    435     if (tinytemplate_compile(line, linelen, program, 256, &num_instr, errmsg, sizeof(errmsg)) !=
    436         TINYTEMPLATE_STATUS_DONE) {
    437       fprintf(stderr, "Warning: template compile failed: %s\n", errmsg);
    438       fputs(line, dst_config);
    439       fputc('\n', dst_config);
    440       continue;
    441     }
    442 
    443     struct template_ctx ctx = {lib_path, dst_config};
    444 
    445     if (tinytemplate_eval(line, program, &ctx, template_getter, template_callback, errmsg, sizeof(errmsg)) !=
    446         TINYTEMPLATE_STATUS_DONE) {
    447       fprintf(stderr, "Warning: template eval failed: %s\n", errmsg);
    448     }
    449     fputc('\n', dst_config);
    450   }
    451 
    452   fputc('\n', dst_config);
    453   fclose(dep_config);
    454   return 0;
    455 }
    456 
    457 static int install_dependency(const char *name, const char *spec) {
    458   char lib_path[PATH_MAX];
    459   snprintf(lib_path, sizeof(lib_path), "lib/%s", name);
    460 
    461   if (dir_exists(lib_path)) {
    462     return 0;
    463   }
    464 
    465   char *url = spec_to_url(name, spec);
    466   if (!url) {
    467     fprintf(stderr, "Error: failed to resolve spec for %s\n", name);
    468     return -1;
    469   }
    470 
    471   printf("Installing %s from %s\n", name, url);
    472 
    473   mkdir_recursive(lib_path);
    474 
    475   if (download_and_extract(url, lib_path) != 0) {
    476     fprintf(stderr, "Error: failed to install %s\n", name);
    477     free(url);
    478     return -1;
    479   }
    480   free(url);
    481 
    482   // Process .dep.chain recursively
    483   char dep_chain_path[PATH_MAX];
    484   while (1) {
    485     snprintf(dep_chain_path, sizeof(dep_chain_path), "%s/.dep.chain", lib_path);
    486 
    487     FILE *chain_file = fopen(dep_chain_path, "r");
    488     if (!chain_file) {
    489       break;
    490     }
    491 
    492     char chain_spec[1024] = {0};
    493     if (!fgets(chain_spec, sizeof(chain_spec), chain_file)) {
    494       fclose(chain_file);
    495       fprintf(stderr, "Error: failed to read .dep.chain\n");
    496       return -1;
    497     }
    498     fclose(chain_file);
    499 
    500     if (remove(dep_chain_path) != 0) {
    501       fprintf(stderr, "Warning: failed to remove .dep.chain\n");
    502     }
    503 
    504     char *trimmed = trim_whitespace(chain_spec);
    505     if (strlen(trimmed) == 0) {
    506       fprintf(stderr, "Warning: empty spec in .dep.chain\n");
    507       continue;
    508     }
    509 
    510     printf("Found .dep.chain, chaining to: %s\n", trimmed);
    511 
    512     char *overlay_url = spec_to_url(name, trimmed);
    513     if (!overlay_url) {
    514       fprintf(stderr, "Error: failed to resolve chained spec '%s'\n", trimmed);
    515       return -1;
    516     }
    517 
    518     printf("Overlaying %s from %s\n", name, overlay_url);
    519     if (download_and_extract(overlay_url, lib_path) != 0) {
    520       fprintf(stderr, "Error: failed to overlay chained dependency\n");
    521       free(overlay_url);
    522       return -1;
    523     }
    524     free(overlay_url);
    525   }
    526 
    527   // Process .dep file in the dependency's directory
    528   if (process_dep_file_in_dir(lib_path) != 0) {
    529     fprintf(stderr, "Warning: failed to process .dep file for %s\n", name);
    530   }
    531 
    532   // Execute postinstall hook if present
    533   if (execute_postinstall_hook(lib_path) != 0) {
    534     fprintf(stderr, "Error: postinstall hook failed for %s\n", name);
    535     return -1;
    536   }
    537 
    538   // Handle .dep.export file
    539   if (process_dep_export_file(lib_path, name) != 0) {
    540     fprintf(stderr, "Warning: failed to process .dep.export file for %s\n", name);
    541   }
    542 
    543   printf("Installed %s\n", name);
    544 
    545   return 0;
    546 }
    547 
    548 static int process_dep_for_config(const char *name, FILE *dst_config) {
    549   // Skip if already processed
    550   if (config_is_visited(name)) {
    551     return 0;
    552   }
    553 
    554   // Recurse into sub-dependencies first
    555   char sub_dep_path[PATH_MAX];
    556   snprintf(sub_dep_path, sizeof(sub_dep_path), "lib/%s/.dep", name);
    557   FILE *f = fopen(sub_dep_path, "r");
    558   if (f) {
    559     char line[LINE_MAX];
    560     while (fgets(line, sizeof(line), f)) {
    561       char *comment = strchr(line, '#');
    562       if (comment) *comment = '\0';
    563 
    564       char *trimmed = trim_whitespace(line);
    565       if (strlen(trimmed) == 0) continue;
    566 
    567       char sub_name[256] = {0};
    568 
    569       char *space_pos        = strchr(trimmed, ' ');
    570       char *tab_pos          = strchr(trimmed, '\t');
    571       char *first_whitespace = NULL;
    572       if (space_pos && tab_pos) {
    573         first_whitespace = (space_pos < tab_pos) ? space_pos : tab_pos;
    574       } else if (space_pos) {
    575         first_whitespace = space_pos;
    576       } else if (tab_pos) {
    577         first_whitespace = tab_pos;
    578       }
    579 
    580       if (first_whitespace) {
    581         size_t name_len = first_whitespace - trimmed;
    582         strncpy(sub_name, trimmed, name_len);
    583         sub_name[name_len] = '\0';
    584       } else {
    585         strncpy(sub_name, trimmed, sizeof(sub_name) - 1);
    586         sub_name[sizeof(sub_name) - 1] = '\0';
    587       }
    588 
    589       if (strlen(sub_name) == 0) continue;
    590 
    591       process_dep_for_config(sub_name, dst_config);
    592     }
    593     fclose(f);
    594   }
    595 
    596   // Mark as visited and add export.mk content
    597   if (config_mark_visited(name) < 0) {
    598     return -1;
    599   }
    600   process_dep_config_mk(name, dst_config);
    601   return 0;
    602 }
    603 
    604 static int rebuild_config_mk(void) {
    605   config_clear_visited();
    606 
    607   char dep_dir[PATH_MAX];
    608   snprintf(dep_dir, sizeof(dep_dir), "lib/.dep");
    609   mkdir_recursive(dep_dir);
    610 
    611   char config_mk_path[PATH_MAX];
    612   snprintf(config_mk_path, sizeof(config_mk_path), "%s/config.mk", dep_dir);
    613 
    614   FILE *config_mk = fopen(config_mk_path, "w");
    615   if (!config_mk) {
    616     fprintf(stderr, "Error: could not create %s\n", config_mk_path);
    617     return -1;
    618   }
    619 
    620   fputs("CFLAGS+=-Ilib/.dep/include\n", config_mk);
    621 
    622   FILE *f = fopen(".dep", "r");
    623   if (!f) {
    624     fclose(config_mk);
    625     return 0;
    626   }
    627 
    628   char line[LINE_MAX];
    629   while (fgets(line, sizeof(line), f)) {
    630     char *comment = strchr(line, '#');
    631     if (comment) *comment = '\0';
    632 
    633     char *trimmed = trim_whitespace(line);
    634     if (strlen(trimmed) == 0) continue;
    635 
    636     char name[256] = {0};
    637 
    638     char *space_pos        = strchr(trimmed, ' ');
    639     char *tab_pos          = strchr(trimmed, '\t');
    640     char *first_whitespace = NULL;
    641     if (space_pos && tab_pos) {
    642       first_whitespace = (space_pos < tab_pos) ? space_pos : tab_pos;
    643     } else if (space_pos) {
    644       first_whitespace = space_pos;
    645     } else if (tab_pos) {
    646       first_whitespace = tab_pos;
    647     }
    648 
    649     if (first_whitespace) {
    650       size_t name_len = first_whitespace - trimmed;
    651       strncpy(name, trimmed, name_len);
    652       name[name_len] = '\0';
    653     } else {
    654       strncpy(name, trimmed, sizeof(name) - 1);
    655       name[sizeof(name) - 1] = '\0';
    656     }
    657 
    658     if (strlen(name) == 0) continue;
    659 
    660     process_dep_for_config(name, config_mk);
    661   }
    662 
    663   fclose(f);
    664   fclose(config_mk);
    665 
    666   config_clear_visited();
    667   return 0;
    668 }
    669 
    670 static int cmd_install(int argc, const char **argv) {
    671   (void)argc;
    672   (void)argv;
    673 
    674   const char *dep_path = ".dep";
    675   FILE       *f        = fopen(dep_path, "r");
    676   if (!f) {
    677     fprintf(stderr, "Error: .dep file not found. Run 'dep init' first.\n");
    678     return 1;
    679   }
    680 
    681   if (!dir_exists("lib")) {
    682     if (mkdir("lib", 0755) != 0) {
    683       fprintf(stderr, "Error: could not create lib directory\n");
    684       fclose(f);
    685       return 1;
    686     }
    687   }
    688 
    689   char dep_dir[PATH_MAX];
    690   snprintf(dep_dir, sizeof(dep_dir), "lib/.dep");
    691   mkdir_recursive(dep_dir);
    692 
    693   char line[LINE_MAX];
    694   int  has_deps = 0;
    695 
    696   // First pass: install all dependencies
    697   while (fgets(line, sizeof(line), f)) {
    698     char *comment = strchr(line, '#');
    699     if (comment) *comment = '\0';
    700 
    701     char *trimmed = trim_whitespace(line);
    702     if (strlen(trimmed) == 0) continue;
    703 
    704     has_deps = 1;
    705 
    706     char name[256]  = {0};
    707     char spec[1024] = {0};
    708 
    709     char *space_pos        = strchr(trimmed, ' ');
    710     char *tab_pos          = strchr(trimmed, '\t');
    711     char *first_whitespace = NULL;
    712     if (space_pos && tab_pos) {
    713       first_whitespace = (space_pos < tab_pos) ? space_pos : tab_pos;
    714     } else if (space_pos) {
    715       first_whitespace = space_pos;
    716     } else if (tab_pos) {
    717       first_whitespace = tab_pos;
    718     }
    719 
    720     if (first_whitespace) {
    721       size_t name_len = first_whitespace - trimmed;
    722       strncpy(name, trimmed, name_len);
    723       name[name_len] = '\0';
    724       strcpy(spec, trim_whitespace(first_whitespace + 1));
    725     } else {
    726       strncpy(name, trimmed, sizeof(name) - 1);
    727       name[sizeof(name) - 1] = '\0';
    728     }
    729 
    730     if (strlen(name) == 0) continue;
    731 
    732     if (install_dependency(name, spec) != 0) {
    733       fclose(f);
    734       return 1;
    735     }
    736   }
    737 
    738   fclose(f);
    739 
    740   // Pass 2: Rebuild config.mk from all installed dependencies
    741   if (rebuild_config_mk() != 0) {
    742     return 1;
    743   }
    744 
    745   if (!has_deps) {
    746     printf("No dependencies to install\n");
    747   }
    748 
    749   return 0;
    750 }
    751 
    752 void __attribute__((constructor)) cmd_install_setup(void) {
    753   struct cmd_struct *cmd = calloc(1, sizeof(struct cmd_struct));
    754   if (!cmd) {
    755     fprintf(stderr, "Failed to allocate memory for install command\n");
    756     return;
    757   }
    758   cmd->next                          = commands;
    759   cmd->fn                            = cmd_install;
    760   static const char *install_names[] = {"install", "i", NULL};
    761   cmd->name                          = install_names;
    762   cmd->display                       = "i(nstall)";
    763   cmd->description                   = "Install all the project's dependencies";
    764   cmd->help_text =
    765       "dep install - Install all the project's dependencies\n"
    766       "\n"
    767       "Usage:\n"
    768       "  dep install\n"
    769       "\n"
    770       "Description:\n"
    771       "  Install all dependencies listed in the .dep file in the current directory.\n"
    772       "\n"
    773       "  Dependencies are installed to the lib/ directory by default.\n"
    774       "\n"
    775       "  Each dependency is downloaded and extracted to lib/<owner>/<name>/.\n"
    776       "\n"
    777       "  If a dependency itself has dependencies listed in its own .dep file,\n"
    778       "  those will be installed recursively.\n";
    779   commands = cmd;
    780 }