main.c (11730B)
1 #include <dirent.h> 2 #include <limits.h> 3 #include <stdio.h> 4 #include <stdlib.h> 5 #include <string.h> 6 #include <sys/stat.h> 7 #include <time.h> 8 #include <unistd.h> 9 10 #include "cofyc/argparse.h" 11 #include "command/command.h" 12 #include "common/fs-utils.h" 13 #include "common/github-utils.h" 14 #include "common/net-utils.h" 15 16 #define CACHE_MAX_AGE_SECONDS (7 * 24 * 60 * 60) 17 18 static unsigned long hash_string(const char *str) { 19 unsigned long hash = 5381; 20 int c; 21 while ((c = *str++)) { 22 hash = ((hash << 5) + hash) + c; 23 } 24 return hash; 25 } 26 27 static void url_to_cache_filename(const char *url, char *filename, size_t size) { 28 unsigned long hash = hash_string(url); 29 snprintf(filename, size, "%lu", hash); 30 } 31 32 static int is_cache_outdated(const char *cache_path) { 33 struct stat st; 34 if (stat(cache_path, &st) != 0) { 35 return 1; 36 } 37 time_t now = time(NULL); 38 time_t file_age = now - st.st_mtime; 39 return file_age > CACHE_MAX_AGE_SECONDS; 40 } 41 42 static int extract_version_from_depname(const char *depname_with_version, char *depname, char *version, 43 size_t depname_size, size_t version_size) { 44 const char *at_pos = strchr(depname_with_version, '@'); 45 if (!at_pos) { 46 strncpy(depname, depname_with_version, depname_size - 1); 47 depname[depname_size - 1] = '\0'; 48 version[0] = '\0'; 49 return 0; 50 } 51 52 size_t depname_len = at_pos - depname_with_version; 53 if (depname_len >= depname_size) { 54 depname_len = depname_size - 1; 55 } 56 strncpy(depname, depname_with_version, depname_len); 57 depname[depname_len] = '\0'; 58 59 strncpy(version, at_pos + 1, version_size - 1); 60 version[version_size - 1] = '\0'; 61 return 0; 62 } 63 64 static int parse_manifest_line(const char *line, char *depname, char *version, char *url, size_t depname_size, 65 size_t version_size, size_t url_size) { 66 char *line_copy = strdup(line); 67 if (!line_copy) return -1; 68 69 char *comment = strchr(line_copy, '#'); 70 if (comment) *comment = '\0'; 71 72 char *trimmed = trim_whitespace(line_copy); 73 if (strlen(trimmed) == 0) { 74 free(line_copy); 75 return -1; 76 } 77 78 char *space_pos = strchr(trimmed, ' '); 79 char *tab_pos = strchr(trimmed, '\t'); 80 char *first_ws = NULL; 81 82 if (space_pos && tab_pos) { 83 first_ws = (space_pos < tab_pos) ? space_pos : tab_pos; 84 } else if (space_pos) { 85 first_ws = space_pos; 86 } else if (tab_pos) { 87 first_ws = tab_pos; 88 } 89 90 int result = 0; 91 92 if (first_ws) { 93 size_t name_len = first_ws - trimmed; 94 if (name_len >= depname_size) { 95 name_len = depname_size - 1; 96 } 97 strncpy(depname, trimmed, name_len); 98 depname[name_len] = '\0'; 99 100 char *url_start = trim_whitespace(first_ws + 1); 101 strncpy(url, url_start, url_size - 1); 102 url[url_size - 1] = '\0'; 103 } else { 104 strncpy(depname, trimmed, depname_size - 1); 105 depname[depname_size - 1] = '\0'; 106 url[0] = '\0'; 107 } 108 109 char version_from_depname[256]; 110 extract_version_from_depname(depname, depname, version_from_depname, depname_size, sizeof(version_from_depname)); 111 112 if (version_from_depname[0] != '\0') { 113 strncpy(version, version_from_depname, version_size - 1); 114 version[version_size - 1] = '\0'; 115 } else { 116 version[0] = '\0'; 117 } 118 119 free(line_copy); 120 return result; 121 } 122 123 static int version_matches(const char *requested, const char *available) { 124 if (!requested || requested[0] == '\0') { 125 return 1; 126 } 127 if (!available || available[0] == '\0') { 128 return 0; 129 } 130 return strcmp(requested, available) == 0; 131 } 132 133 static int append_to_dep_file(const char *name, const char *spec) { 134 const char *dep_path = ".dep"; 135 FILE *f = fopen(dep_path, "a"); 136 if (!f) { 137 fprintf(stderr, "Error: could not open .dep file for writing\n"); 138 return -1; 139 } 140 141 fseek(f, 0, SEEK_END); 142 long pos = ftell(f); 143 if (pos > 0) { 144 fputc('\n', f); 145 } 146 147 if (spec && spec[0] != '\0') { 148 fprintf(f, "%s %s\n", name, spec); 149 } else { 150 fprintf(f, "%s\n", name); 151 } 152 153 fclose(f); 154 return 0; 155 } 156 157 static int add_from_url(const char *name, const char *url) { 158 int result = append_to_dep_file(name, url); 159 if (result == 0) { 160 printf("Added %s to .dep\n", name); 161 } 162 return result; 163 } 164 165 static int add_from_repository(const char *name, const char *requested_version) { 166 char *repo_dir = get_repo_dir(); 167 if (!repo_dir) { 168 return -1; 169 } 170 171 char *cache_dir = get_cache_dir(); 172 if (!cache_dir) { 173 free(repo_dir); 174 return -1; 175 } 176 177 mkdir_recursive(cache_dir); 178 179 DIR *dir = opendir(repo_dir); 180 if (!dir) { 181 fprintf(stderr, "Error: could not open repository directory\n"); 182 free(repo_dir); 183 free(cache_dir); 184 return -1; 185 } 186 187 struct dirent *entry; 188 int found = 0; 189 190 while (!found && (entry = readdir(dir)) != NULL) { 191 if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, "..")) continue; 192 193 char *repo_dir2 = get_repo_dir(); 194 if (!repo_dir2) continue; 195 196 char filepath[PATH_MAX]; 197 snprintf(filepath, sizeof(filepath), "%s%s", repo_dir2, entry->d_name); 198 free(repo_dir2); 199 200 struct stat st; 201 if (stat(filepath, &st) < 0 || !S_ISREG(st.st_mode)) continue; 202 203 FILE *repo_file = fopen(filepath, "r"); 204 if (!repo_file) continue; 205 206 char line[LINE_MAX]; 207 while (!found && fgets(line, sizeof(line), repo_file)) { 208 char *comment = strchr(line, '#'); 209 if (comment) *comment = '\0'; 210 211 char *trimmed = trim_whitespace(line); 212 if (strlen(trimmed) == 0) continue; 213 214 char repo_name[256] = {0}; 215 char manifest_url[1024] = {0}; 216 217 char *space_pos = strchr(trimmed, ' '); 218 char *tab_pos = strchr(trimmed, '\t'); 219 char *first_ws = NULL; 220 221 if (space_pos && tab_pos) { 222 first_ws = (space_pos < tab_pos) ? space_pos : tab_pos; 223 } else if (space_pos) { 224 first_ws = space_pos; 225 } else if (tab_pos) { 226 first_ws = tab_pos; 227 } 228 229 if (!first_ws) continue; 230 231 size_t name_len = first_ws - trimmed; 232 if (name_len >= sizeof(repo_name)) { 233 name_len = sizeof(repo_name) - 1; 234 } 235 strncpy(repo_name, trimmed, name_len); 236 repo_name[name_len] = '\0'; 237 238 char *url_start = trim_whitespace(first_ws + 1); 239 strncpy(manifest_url, url_start, sizeof(manifest_url) - 1); 240 manifest_url[sizeof(manifest_url) - 1] = '\0'; 241 242 if (strlen(manifest_url) == 0) continue; 243 244 char cache_filename[256]; 245 url_to_cache_filename(manifest_url, cache_filename, sizeof(cache_filename)); 246 247 char cache_path[PATH_MAX]; 248 snprintf(cache_path, sizeof(cache_path), "%s%s", cache_dir, cache_filename); 249 250 if (is_cache_outdated(cache_path)) { 251 printf("Updating cache for repository %s...\n", repo_name); 252 size_t size; 253 char *manifest_content = download_url(manifest_url, &size); 254 if (manifest_content) { 255 FILE *cache_file = fopen(cache_path, "w"); 256 if (cache_file) { 257 fwrite(manifest_content, 1, size, cache_file); 258 fclose(cache_file); 259 } 260 free(manifest_content); 261 } 262 } 263 264 FILE *cache_file = fopen(cache_path, "r"); 265 if (!cache_file) continue; 266 267 char manifest_line[LINE_MAX]; 268 while (!found && fgets(manifest_line, sizeof(manifest_line), cache_file)) { 269 char depname[256] = {0}; 270 char version[256] = {0}; 271 char tarball_url[1024] = {0}; 272 273 if (parse_manifest_line(manifest_line, depname, version, tarball_url, sizeof(depname), sizeof(version), 274 sizeof(tarball_url)) != 0) { 275 continue; 276 } 277 278 if (strcmp(depname, name) != 0) continue; 279 280 if (!version_matches(requested_version, version)) continue; 281 282 if (strlen(tarball_url) == 0) continue; 283 284 printf("Found %s", name); 285 if (version[0] != '\0') { 286 printf(" version %s", version); 287 } 288 printf(" in repository %s\n", repo_name); 289 290 fclose(cache_file); 291 closedir(dir); 292 free(repo_dir); 293 free(cache_dir); 294 return append_to_dep_file(name, tarball_url); 295 } 296 297 fclose(cache_file); 298 } 299 300 fclose(repo_file); 301 } 302 303 closedir(dir); 304 free(repo_dir); 305 free(cache_dir); 306 307 return found ? 0 : -1; 308 } 309 310 static int add_from_github(const char *name, const char *requested_version) { 311 char *full_ref = NULL; 312 313 if (requested_version && requested_version[0] != '\0') { 314 full_ref = github_matching_ref(name, requested_version); 315 if (!full_ref) { 316 fprintf(stderr, "Error: ref '%s' not found for %s on GitHub\n", requested_version, name); 317 return -1; 318 } 319 free(full_ref); 320 printf("Found %s version %s on GitHub\n", name, requested_version); 321 fflush(stdout); 322 return append_to_dep_file(name, requested_version); 323 } else { 324 char *branch = github_default_branch(name); 325 if (!branch) { 326 fprintf(stderr, "Error: could not determine default branch for %s on GitHub\n", name); 327 return -1; 328 } 329 printf("Found %s on GitHub (default branch: %s)\n", name, branch); 330 fflush(stdout); 331 free(branch); 332 return append_to_dep_file(name, ""); 333 } 334 } 335 336 static int cmd_add(int argc, const char **argv) { 337 struct argparse_option options[] = { 338 OPT_HELP(), 339 OPT_END(), 340 }; 341 342 struct argparse argparse; 343 argparse_init(&argparse, options, NULL, 0); 344 argc = argparse_parse(&argparse, argc, argv); 345 346 if (argc < 1) { 347 fprintf(stderr, "Error: add requires <name> [version]\n"); 348 fprintf(stderr, "Usage: dep add <name> [version]\n"); 349 return 1; 350 } 351 352 const char *name = argv[0]; 353 const char *version = (argc > 1) ? argv[1] : NULL; 354 355 if (version && is_url(version)) { 356 printf("Adding %s as direct URL: %s\n", name, version); 357 return add_from_url(name, version); 358 } 359 360 printf("Searching repositories for %s", name); 361 if (version && strlen(version) > 0) { 362 printf(" with version %s", version); 363 } 364 printf("...\n"); 365 fflush(stdout); 366 367 int result = add_from_repository(name, version); 368 if (result == 0) { 369 printf("Added %s to .dep\n", name); 370 fflush(stdout); 371 return 0; 372 } 373 374 printf("Not found in repositories, trying GitHub...\n"); 375 fflush(stdout); 376 result = add_from_github(name, version); 377 if (result == 0) { 378 printf("Added %s to .dep\n", name); 379 return 0; 380 } 381 382 fprintf(stderr, "Error: package '%s' not found\n", name); 383 return 1; 384 } 385 386 void __attribute__((constructor)) cmd_add_setup(void) { 387 struct cmd_struct *cmd = calloc(1, sizeof(struct cmd_struct)); 388 if (!cmd) { 389 fprintf(stderr, "Failed to allocate memory for add command\n"); 390 return; 391 } 392 cmd->next = commands; 393 cmd->fn = cmd_add; 394 static const char *add_names[] = {"add", "a", NULL}; 395 cmd->name = add_names; 396 cmd->display = "a(dd)"; 397 cmd->description = "Add a new dependency to the project"; 398 cmd->help_text = 399 "dep add - Add a new dependency to the project\n" 400 "\n" 401 "Usage:\n" 402 " dep add <name>\n" 403 " dep add <name> <version>\n" 404 " dep add <name> <url>\n" 405 "\n" 406 "Description:\n" 407 " Add a package to the project's .dep file.\n" 408 "\n" 409 " If a version is not specified, the latest version from the repository\n" 410 " will be used, or the default branch from GitHub.\n" 411 "\n" 412 " You can also add a dependency with a direct URL.\n" 413 "\n" 414 "Examples:\n" 415 " dep add finwo/palloc # Latest from repo or GitHub main branch\n" 416 " dep add finwo/palloc edge # Specific version/branch\n" 417 " dep add finwo/palloc v1.0.0 # Specific tag\n" 418 " dep add mylib https://example.com/mylib.tar.gz\n"; 419 commands = cmd; 420 }