dep

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

main.c (17233B)


      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 
     31 static int dir_exists(const char *path) {
     32   struct stat st;
     33   return stat(path, &st) == 0 && S_ISDIR(st.st_mode);
     34 }
     35 
     36 static int mkdir_recursive(const char *path) {
     37   char   tmp[PATH_MAX];
     38   char  *p = NULL;
     39   size_t len;
     40 
     41   snprintf(tmp, sizeof(tmp), "%s", path);
     42   len = strlen(tmp);
     43   if (tmp[len - 1] == '/') {
     44     tmp[len - 1] = '\0';
     45   }
     46 
     47   for (p = tmp + 1; *p; p++) {
     48     if (*p == '/') {
     49       *p = '\0';
     50       mkdir(tmp, 0755);
     51       *p = '/';
     52     }
     53   }
     54   return mkdir(tmp, 0755);
     55 }
     56 
     57 static char *trim_whitespace(char *str) {
     58   while (*str == ' ' || *str == '\t' || *str == '\n' || *str == '\r') str++;
     59   if (*str == '\0') return str;
     60   char *end = str + strlen(str) - 1;
     61   while (end > str && (*end == ' ' || *end == '\t' || *end == '\n' || *end == '\r')) {
     62     *end = '\0';
     63     end--;
     64   }
     65   return str;
     66 }
     67 
     68 static char *spec_to_url(const char *name, const char *spec) {
     69   if (strlen(spec) > 0 && is_url(spec)) {
     70     return strdup(spec);
     71   }
     72 
     73   char *full_ref = NULL;
     74 
     75   if (strlen(spec) > 0) {
     76     full_ref = github_matching_ref(name, spec);
     77     if (!full_ref) {
     78       fprintf(stderr, "Error: ref '%s' not found for %s\n", spec, name);
     79       return NULL;
     80     }
     81   } else {
     82     char *branch = github_default_branch(name);
     83     if (!branch) {
     84       fprintf(stderr, "Warning: could not determine default branch for %s, using 'main'\n", name);
     85       branch = strdup("main");
     86     }
     87     full_ref = malloc(256);
     88     if (full_ref) {
     89       snprintf(full_ref, 256, "refs/heads/%s", branch);
     90     }
     91     free(branch);
     92   }
     93 
     94   if (!full_ref) {
     95     fprintf(stderr, "Error: could not determine ref for %s\n", name);
     96     return NULL;
     97   }
     98 
     99   char *url = malloc(2048);
    100   if (!url) {
    101     free(full_ref);
    102     return NULL;
    103   }
    104   snprintf(url, 2048, "https://github.com/%s/archive/%s.tar.gz", name, full_ref);
    105   free(full_ref);
    106   return url;
    107 }
    108 
    109 static int process_dep_export_file(const char *dep_dir, const char *name) {
    110   char export_path[PATH_MAX];
    111   snprintf(export_path, sizeof(export_path), "%s/.dep.export", dep_dir);
    112   FILE *f = fopen(export_path, "r");
    113   if (!f) {
    114     return 0;
    115   }
    116 
    117   char dep_base[PATH_MAX];
    118   if (getcwd(dep_base, sizeof(dep_base)) == NULL) {
    119     fprintf(stderr, "Error: failed to get current working directory\n");
    120     fclose(f);
    121     return -1;
    122   }
    123 
    124   char line[PATH_MAX];
    125   while (fgets(line, sizeof(line), f)) {
    126     char *comment = strchr(line, '#');
    127     if (comment) *comment = '\0';
    128 
    129     char *trimmed = trim_whitespace(line);
    130     if (strlen(trimmed) == 0) continue;
    131 
    132     char source[512] = {0};
    133     char target[512] = {0};
    134 
    135     char *space_pos        = strchr(trimmed, ' ');
    136     char *tab_pos          = strchr(trimmed, '\t');
    137     char *first_whitespace = NULL;
    138     if (space_pos && tab_pos) {
    139       first_whitespace = (space_pos < tab_pos) ? space_pos : tab_pos;
    140     } else if (space_pos) {
    141       first_whitespace = space_pos;
    142     } else if (tab_pos) {
    143       first_whitespace = tab_pos;
    144     }
    145 
    146     if (first_whitespace) {
    147       size_t source_len = first_whitespace - trimmed;
    148       strncpy(source, trimmed, source_len);
    149       source[source_len] = '\0';
    150       strcpy(target, trim_whitespace(first_whitespace + 1));
    151     } else {
    152       continue;
    153     }
    154 
    155     if (strlen(source) == 0 || strlen(target) == 0) continue;
    156 
    157     char source_parent[PATH_MAX];
    158     strncpy(source_parent, source, sizeof(source_parent) - 1);
    159     source_parent[sizeof(source_parent) - 1] = '\0';
    160     char *last_slash                         = strrchr(source_parent, '/');
    161     if (last_slash) {
    162       *last_slash = '\0';
    163       char parent_dir[PATH_MAX];
    164       snprintf(parent_dir, sizeof(parent_dir), "lib/.dep/%s", source_parent);
    165       mkdir_recursive(parent_dir);
    166     } else {
    167       mkdir_recursive("lib/.dep");
    168     }
    169 
    170     char target_abs[PATH_MAX];
    171     snprintf(target_abs, sizeof(target_abs), "%s/lib/%s/%s", dep_base, name, target);
    172 
    173     char link_path[PATH_MAX];
    174     snprintf(link_path, sizeof(link_path), "lib/.dep/%s", source);
    175 
    176     unlink(link_path);
    177     if (symlink(target_abs, link_path) != 0) {
    178       fprintf(stderr, "Warning: failed to create symlink %s -> %s\n", link_path, target_abs);
    179     } else {
    180       printf("Exported %s -> %s\n", source, target_abs);
    181     }
    182   }
    183 
    184   fclose(f);
    185   return 0;
    186 }
    187 
    188 static int process_dep_file_in_dir(const char *dep_dir);
    189 static int execute_postinstall_hook(const char *dep_dir);
    190 
    191 static int process_dep_file_in_dir(const char *dep_dir) {
    192   char dep_path[PATH_MAX];
    193   snprintf(dep_path, sizeof(dep_path), "%s/.dep", dep_dir);
    194   FILE *f = fopen(dep_path, "r");
    195   if (!f) {
    196     // No .dep file is not an error
    197     return 0;
    198   }
    199 
    200   char line[LINE_MAX];
    201   while (fgets(line, sizeof(line), f)) {
    202     char *comment = strchr(line, '#');
    203     if (comment) *comment = '\0';
    204 
    205     char *trimmed = trim_whitespace(line);
    206     if (strlen(trimmed) == 0) continue;
    207 
    208     char name[256]  = {0};
    209     char spec[1024] = {0};
    210 
    211     char *space_pos        = strchr(trimmed, ' ');
    212     char *tab_pos          = strchr(trimmed, '\t');
    213     char *first_whitespace = NULL;
    214     if (space_pos && tab_pos) {
    215       first_whitespace = (space_pos < tab_pos) ? space_pos : tab_pos;
    216     } else if (space_pos) {
    217       first_whitespace = space_pos;
    218     } else if (tab_pos) {
    219       first_whitespace = tab_pos;
    220     }
    221 
    222     if (first_whitespace) {
    223       size_t name_len = first_whitespace - trimmed;
    224       strncpy(name, trimmed, name_len);
    225       name[name_len] = '\0';
    226       strcpy(spec, trim_whitespace(first_whitespace + 1));
    227     } else {
    228       strncpy(name, trimmed, sizeof(name) - 1);
    229       name[sizeof(name) - 1] = '\0';
    230     }
    231 
    232     if (strlen(name) == 0) continue;
    233 
    234     install_dependency(name, spec);
    235   }
    236 
    237   fclose(f);
    238   return 0;
    239 }
    240 
    241 static int execute_postinstall_hook(const char *dep_dir) {
    242   char cwd[PATH_MAX];
    243   if (getcwd(cwd, sizeof(cwd)) == NULL) {
    244     fprintf(stderr, "Error: failed to get current working directory\n");
    245     return -1;
    246   }
    247 
    248   if (chdir(dep_dir) != 0) {
    249     fprintf(stderr, "Error: failed to change to dependency directory\n");
    250     return -1;
    251   }
    252 
    253   char hook_path[PATH_MAX];
    254   snprintf(hook_path, sizeof(hook_path), "./.dep.hook.postinstall");
    255 
    256   struct stat st;
    257   if (stat(hook_path, &st) != 0) {
    258     chdir(cwd);
    259     return 0;
    260   }
    261 
    262   int exec_bits = S_IXUSR | S_IXGRP | S_IXOTH;
    263   if (!(st.st_mode & exec_bits)) {
    264     fprintf(stderr, "Warning: %s is not executable, making executable...\n", hook_path);
    265     if (chmod(hook_path, st.st_mode | exec_bits) != 0) {
    266       fprintf(stderr, "Error: failed to make hook executable\n");
    267       chdir(cwd);
    268       return -1;
    269     }
    270     if (stat(hook_path, &st) != 0) {
    271       chdir(cwd);
    272       return 0;
    273     }
    274   }
    275 
    276   pid_t pid = fork();
    277   if (pid == 0) {
    278     char *const argv[] = {hook_path, NULL};
    279     execve(hook_path, argv, environ);
    280     fprintf(stderr, "Error: execve failed: errno=%d\n", errno);
    281     _exit(127);
    282   } else if (pid < 0) {
    283     chdir(cwd);
    284     fprintf(stderr, "Error: failed to fork for postinstall hook\n");
    285     return -1;
    286   }
    287 
    288   int status;
    289   waitpid(pid, &status, 0);
    290 
    291   chdir(cwd);
    292 
    293   if (WIFEXITED(status) && WEXITSTATUS(status) == 0) {
    294     printf("Executed postinstall hook for %s\n", dep_dir);
    295     return 0;
    296   } else {
    297     fprintf(stderr, "Error: postinstall hook failed with exit code %d\n", WIFEXITED(status) ? WEXITSTATUS(status) : -1);
    298     return -1;
    299   }
    300 }
    301 
    302 static int install_dependency(const char *name, const char *spec);
    303 
    304 struct template_ctx {
    305   const char *lib_path;
    306   FILE       *fp;
    307 };
    308 
    309 static bool module_dict_getter(void *data, const char *name, size_t len, tinytemplate_value_t *value) {
    310   const char *lib_path = data;
    311   if (len == 7 && strncmp(name, "dirname", 7) == 0) {
    312     tinytemplate_set_string(value, lib_path, strlen(lib_path));
    313     return true;
    314   }
    315   char orig[256];
    316   int  n = snprintf(orig, sizeof(orig), "{%.*s}", (int)len, name);
    317   tinytemplate_set_string(value, orig, n);
    318   return true;
    319 }
    320 
    321 static bool template_getter(void *data, const char *name, size_t len, tinytemplate_value_t *value) {
    322   struct template_ctx *ctx = data;
    323 
    324   if (len == 6 && strncmp(name, "module", 6) == 0) {
    325     tinytemplate_set_dict(value, (void *)ctx->lib_path, module_dict_getter);
    326     return true;
    327   }
    328 
    329   char orig[256];
    330   int  n = snprintf(orig, sizeof(orig), "{{%.*s}}", (int)len, name);
    331   tinytemplate_set_string(value, orig, n);
    332   return true;
    333 }
    334 
    335 static void template_callback(void *data, const char *str, size_t len) {
    336   struct template_ctx *ctx = data;
    337   fwrite(str, 1, len, ctx->fp);
    338 }
    339 
    340 static int install_dependency(const char *name, const char *spec) {
    341   char lib_path[PATH_MAX];
    342   snprintf(lib_path, sizeof(lib_path), "lib/%s", name);
    343 
    344   if (dir_exists(lib_path)) {
    345     printf("Skipping %s (already installed)\n", name);
    346     return 0;
    347   }
    348 
    349   char *url = spec_to_url(name, spec);
    350   if (!url) {
    351     fprintf(stderr, "Error: failed to resolve spec for %s\n", name);
    352     return -1;
    353   }
    354 
    355   printf("Installing %s from %s\n", name, url);
    356 
    357   mkdir_recursive(lib_path);
    358 
    359   if (download_and_extract(url, lib_path) != 0) {
    360     fprintf(stderr, "Error: failed to install %s\n", name);
    361     free(url);
    362     return -1;
    363   }
    364   free(url);
    365 
    366   // Process .dep.chain recursively
    367   char dep_chain_path[PATH_MAX];
    368   while (1) {
    369     // Build .dep.chain path
    370     snprintf(dep_chain_path, sizeof(dep_chain_path), "%s/.dep.chain", lib_path);
    371 
    372     // Check if .dep.chain exists
    373     FILE *chain_file = fopen(dep_chain_path, "r");
    374     if (!chain_file) {
    375       break;  // No more chaining
    376     }
    377 
    378     // Read spec (single line)
    379     char spec[1024] = {0};
    380     if (!fgets(spec, sizeof(spec), chain_file)) {
    381       fclose(chain_file);
    382       fprintf(stderr, "Error: failed to read .dep.chain\n");
    383       return -1;
    384     }
    385     fclose(chain_file);
    386 
    387     // Delete .dep.chain file
    388     if (remove(dep_chain_path) != 0) {
    389       fprintf(stderr, "Warning: failed to remove .dep.chain\n");
    390       // Continue anyway - spec was read
    391     }
    392 
    393     // Trim whitespace/newline from spec
    394     char *trimmed = trim_whitespace(spec);
    395     if (strlen(trimmed) == 0) {
    396       fprintf(stderr, "Warning: empty spec in .dep.chain\n");
    397       continue;
    398     }
    399 
    400     printf("Found .dep.chain, chaining to: %s\n", trimmed);
    401 
    402     // Resolve spec to URL
    403     char *overlay_url = spec_to_url(name, trimmed);
    404     if (!overlay_url) {
    405       fprintf(stderr, "Error: failed to resolve chained spec '%s'\n", trimmed);
    406       return -1;
    407     }
    408 
    409     // Overlay extract (directly over existing files)
    410     printf("Overlaying %s from %s\n", name, overlay_url);
    411     if (download_and_extract(overlay_url, lib_path) != 0) {
    412       fprintf(stderr, "Error: failed to overlay chained dependency\n");
    413       free(overlay_url);
    414       return -1;
    415     }
    416     free(overlay_url);
    417     // Loop continues to check for new .dep.chain
    418   }
    419 
    420   // Process .dep file in the dependency's directory
    421   if (process_dep_file_in_dir(lib_path) != 0) {
    422     fprintf(stderr, "Warning: failed to process .dep file for %s\n", name);
    423     // Not returning error because the dependency itself installed successfully.
    424   }
    425 
    426   // Execute postinstall hook if present
    427   if (execute_postinstall_hook(lib_path) != 0) {
    428     fprintf(stderr, "Error: postinstall hook failed for %s\n", name);
    429     return -1;
    430   }
    431 
    432   // Handle config.mk: append dependency's config.mk to lib/.dep/config.mk
    433   char dep_dir[PATH_MAX];
    434   snprintf(dep_dir, sizeof(dep_dir), "lib/.dep");
    435   mkdir_recursive(dep_dir);
    436 
    437   char src_config_path[PATH_MAX];
    438   snprintf(src_config_path, sizeof(src_config_path), "%s/config.mk", lib_path);
    439 
    440   char dst_config_path[PATH_MAX];
    441   snprintf(dst_config_path, sizeof(dst_config_path), "lib/.dep/config.mk");
    442 
    443   FILE *dep_config = fopen(src_config_path, "r");
    444   if (dep_config) {
    445     FILE *dst_config = fopen(dst_config_path, "a");
    446     if (dst_config) {
    447       char line[LINE_MAX];
    448       while (fgets(line, sizeof(line), dep_config)) {
    449         size_t linelen = strlen(line);
    450         if (linelen > 0 && line[linelen - 1] == '\n') {
    451           line[--linelen] = '\0';
    452         }
    453         if (linelen > 0 && line[linelen - 1] == '\r') {
    454           line[--linelen] = '\0';
    455         }
    456 
    457         tinytemplate_instr_t program[256];
    458         size_t               num_instr = 0;
    459         char                 errmsg[256];
    460 
    461         if (linelen == 0) {
    462           fputc('\n', dst_config);
    463           continue;
    464         }
    465 
    466         if (tinytemplate_compile(line, linelen, program, 256, &num_instr, errmsg, sizeof(errmsg)) !=
    467             TINYTEMPLATE_STATUS_DONE) {
    468           fprintf(stderr, "Warning: template compile failed: %s\n", errmsg);
    469           fputs(line, dst_config);
    470           fputc('\n', dst_config);
    471           continue;
    472         }
    473 
    474         struct template_ctx ctx = {lib_path, dst_config};
    475 
    476         if (tinytemplate_eval(line, program, &ctx, template_getter, template_callback, errmsg, sizeof(errmsg)) !=
    477             TINYTEMPLATE_STATUS_DONE) {
    478           fprintf(stderr, "Warning: template eval failed: %s\n", errmsg);
    479         }
    480         fputc('\n', dst_config);
    481       }
    482       // Ensure a newline at end of appended content if not already ending with newline
    483       // (optional, but we can add a newline to separate entries)
    484       fputc('\n', dst_config);
    485       fclose(dst_config);
    486     } else {
    487       fprintf(stderr, "Warning: could not open %s for appending\n", dst_config_path);
    488     }
    489     fclose(dep_config);
    490   }
    491 
    492   if (process_dep_export_file(lib_path, name) != 0) {
    493     fprintf(stderr, "Warning: failed to process .dep.export file for %s\n", name);
    494   }
    495 
    496   printf("Installed %s\n", name);
    497   return 0;
    498 }
    499 
    500 static int cmd_install(int argc, const char **argv) {
    501   (void)argc;
    502   (void)argv;
    503 
    504   const char *dep_path = ".dep";
    505   FILE       *f        = fopen(dep_path, "r");
    506   if (!f) {
    507     fprintf(stderr, "Error: .dep file not found. Run 'dep init' first.\n");
    508     return 1;
    509   }
    510 
    511   if (!dir_exists("lib")) {
    512     if (mkdir("lib", 0755) != 0) {
    513       fprintf(stderr, "Error: could not create lib directory\n");
    514       fclose(f);
    515       return 1;
    516     }
    517   }
    518 
    519   char dep_dir[PATH_MAX];
    520   snprintf(dep_dir, sizeof(dep_dir), "lib/.dep");
    521   mkdir_recursive(dep_dir);
    522 
    523   char config_mk_path[PATH_MAX];
    524   snprintf(config_mk_path, sizeof(config_mk_path), "%s/config.mk", dep_dir);
    525   FILE *config_mk = fopen(config_mk_path, "w");
    526   if (config_mk) {
    527     fputs("CFLAGS+=-Ilib/.dep/include\n", config_mk);
    528     fclose(config_mk);
    529   } else {
    530     fprintf(stderr, "Warning: could not create %s\n", config_mk_path);
    531   }
    532 
    533   char line[LINE_MAX];
    534   int  has_deps = 0;
    535 
    536   while (fgets(line, sizeof(line), f)) {
    537     char *comment = strchr(line, '#');
    538     if (comment) *comment = '\0';
    539 
    540     char *trimmed = trim_whitespace(line);
    541     if (strlen(trimmed) == 0) continue;
    542 
    543     has_deps = 1;
    544 
    545     char name[256]  = {0};
    546     char spec[1024] = {0};
    547 
    548     char *space_pos        = strchr(trimmed, ' ');
    549     char *tab_pos          = strchr(trimmed, '\t');
    550     char *first_whitespace = NULL;
    551     if (space_pos && tab_pos) {
    552       first_whitespace = (space_pos < tab_pos) ? space_pos : tab_pos;
    553     } else if (space_pos) {
    554       first_whitespace = space_pos;
    555     } else if (tab_pos) {
    556       first_whitespace = tab_pos;
    557     }
    558 
    559     if (first_whitespace) {
    560       size_t name_len = first_whitespace - trimmed;
    561       strncpy(name, trimmed, name_len);
    562       name[name_len] = '\0';
    563       strcpy(spec, trim_whitespace(first_whitespace + 1));
    564     } else {
    565       strncpy(name, trimmed, sizeof(name) - 1);
    566       name[sizeof(name) - 1] = '\0';
    567     }
    568 
    569     if (strlen(name) == 0) continue;
    570 
    571     install_dependency(name, spec);
    572   }
    573 
    574   fclose(f);
    575 
    576   if (!has_deps) {
    577     printf("No dependencies to install\n");
    578   }
    579 
    580   return 0;
    581 }
    582 
    583 void __attribute__((constructor)) cmd_install_setup(void) {
    584   struct cmd_struct *cmd = calloc(1, sizeof(struct cmd_struct));
    585   if (!cmd) {
    586     fprintf(stderr, "Failed to allocate memory for install command\n");
    587     return;
    588   }
    589   cmd->next                          = commands;
    590   cmd->fn                            = cmd_install;
    591   static const char *install_names[] = {"install", "i", NULL};
    592   cmd->name                          = install_names;
    593   cmd->display                       = "i(nstall)";
    594   cmd->description                   = "Install all the project's dependencies";
    595   cmd->help_text =
    596       "dep install - Install all the project's dependencies\n"
    597       "\n"
    598       "Usage:\n"
    599       "  dep install\n"
    600       "\n"
    601       "Description:\n"
    602       "  Install all dependencies listed in the .dep file in the current directory.\n"
    603       "\n"
    604       "  Dependencies are installed to the lib/ directory by default.\n"
    605       "\n"
    606       "  Each dependency is downloaded and extracted to lib/<owner>/<name>/.\n"
    607       "\n"
    608       "  If a dependency itself has dependencies listed in its own .dep file,\n"
    609       "  those will be installed recursively.\n";
    610   commands = cmd;
    611 }