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 }