main.c (11871B)
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 depname_copy[256]; 110 strncpy(depname_copy, depname, sizeof(depname_copy) - 1); 111 depname_copy[sizeof(depname_copy) - 1] = '\0'; 112 113 char version_from_depname[256]; 114 extract_version_from_depname(depname_copy, depname, version_from_depname, depname_size, sizeof(version_from_depname)); 115 116 if (version_from_depname[0] != '\0') { 117 strncpy(version, version_from_depname, version_size - 1); 118 version[version_size - 1] = '\0'; 119 } else { 120 version[0] = '\0'; 121 } 122 123 free(line_copy); 124 return result; 125 } 126 127 static int version_matches(const char *requested, const char *available) { 128 if (!requested || requested[0] == '\0') { 129 return 1; 130 } 131 if (!available || available[0] == '\0') { 132 return 0; 133 } 134 return strcmp(requested, available) == 0; 135 } 136 137 static int append_to_dep_file(const char *name, const char *spec) { 138 const char *dep_path = ".dep"; 139 FILE *f = fopen(dep_path, "a"); 140 if (!f) { 141 fprintf(stderr, "Error: could not open .dep file for writing\n"); 142 return -1; 143 } 144 145 fseek(f, 0, SEEK_END); 146 long pos = ftell(f); 147 if (pos > 0) { 148 fputc('\n', f); 149 } 150 151 if (spec && spec[0] != '\0') { 152 fprintf(f, "%s %s\n", name, spec); 153 } else { 154 fprintf(f, "%s\n", name); 155 } 156 157 fclose(f); 158 return 0; 159 } 160 161 static int add_from_url(const char *name, const char *url) { 162 int result = append_to_dep_file(name, url); 163 if (result == 0) { 164 printf("Added %s to .dep\n", name); 165 } 166 return result; 167 } 168 169 static int add_from_repository(const char *name, const char *requested_version) { 170 char *repo_dir = get_repo_dir(); 171 if (!repo_dir) { 172 return -1; 173 } 174 175 char *cache_dir = get_cache_dir(); 176 if (!cache_dir) { 177 free(repo_dir); 178 return -1; 179 } 180 181 mkdir_recursive(cache_dir); 182 183 DIR *dir = opendir(repo_dir); 184 if (!dir) { 185 fprintf(stderr, "Error: could not open repository directory\n"); 186 free(repo_dir); 187 free(cache_dir); 188 return -1; 189 } 190 191 struct dirent *entry; 192 int found = 0; 193 194 while (!found && (entry = readdir(dir)) != NULL) { 195 if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, "..")) continue; 196 197 char *repo_dir2 = get_repo_dir(); 198 if (!repo_dir2) continue; 199 200 char filepath[PATH_MAX]; 201 snprintf(filepath, sizeof(filepath), "%s%s", repo_dir2, entry->d_name); 202 free(repo_dir2); 203 204 struct stat st; 205 if (stat(filepath, &st) < 0 || !S_ISREG(st.st_mode)) continue; 206 207 FILE *repo_file = fopen(filepath, "r"); 208 if (!repo_file) continue; 209 210 char line[LINE_MAX]; 211 while (!found && fgets(line, sizeof(line), repo_file)) { 212 char *comment = strchr(line, '#'); 213 if (comment) *comment = '\0'; 214 215 char *trimmed = trim_whitespace(line); 216 if (strlen(trimmed) == 0) continue; 217 218 char repo_name[256] = {0}; 219 char manifest_url[1024] = {0}; 220 221 char *space_pos = strchr(trimmed, ' '); 222 char *tab_pos = strchr(trimmed, '\t'); 223 char *first_ws = NULL; 224 225 if (space_pos && tab_pos) { 226 first_ws = (space_pos < tab_pos) ? space_pos : tab_pos; 227 } else if (space_pos) { 228 first_ws = space_pos; 229 } else if (tab_pos) { 230 first_ws = tab_pos; 231 } 232 233 if (!first_ws) continue; 234 235 size_t name_len = first_ws - trimmed; 236 if (name_len >= sizeof(repo_name)) { 237 name_len = sizeof(repo_name) - 1; 238 } 239 strncpy(repo_name, trimmed, name_len); 240 repo_name[name_len] = '\0'; 241 242 char *url_start = trim_whitespace(first_ws + 1); 243 strncpy(manifest_url, url_start, sizeof(manifest_url) - 1); 244 manifest_url[sizeof(manifest_url) - 1] = '\0'; 245 246 if (strlen(manifest_url) == 0) continue; 247 248 char cache_filename[256]; 249 url_to_cache_filename(manifest_url, cache_filename, sizeof(cache_filename)); 250 251 char cache_path[PATH_MAX]; 252 snprintf(cache_path, sizeof(cache_path), "%s%s", cache_dir, cache_filename); 253 254 if (is_cache_outdated(cache_path)) { 255 printf("Updating cache for repository %s...\n", repo_name); 256 size_t size; 257 char *manifest_content = download_url(manifest_url, &size); 258 if (manifest_content) { 259 FILE *cache_file = fopen(cache_path, "w"); 260 if (cache_file) { 261 fwrite(manifest_content, 1, size, cache_file); 262 fclose(cache_file); 263 } 264 free(manifest_content); 265 } 266 } 267 268 FILE *cache_file = fopen(cache_path, "r"); 269 if (!cache_file) continue; 270 271 char manifest_line[LINE_MAX]; 272 while (!found && fgets(manifest_line, sizeof(manifest_line), cache_file)) { 273 char depname[256] = {0}; 274 char version[256] = {0}; 275 char tarball_url[1024] = {0}; 276 277 if (parse_manifest_line(manifest_line, depname, version, tarball_url, sizeof(depname), sizeof(version), 278 sizeof(tarball_url)) != 0) { 279 continue; 280 } 281 282 if (strcmp(depname, name) != 0) continue; 283 284 if (!version_matches(requested_version, version)) continue; 285 286 if (strlen(tarball_url) == 0) continue; 287 288 printf("Found %s", name); 289 if (version[0] != '\0') { 290 printf(" version %s", version); 291 } 292 printf(" in repository %s\n", repo_name); 293 294 fclose(cache_file); 295 closedir(dir); 296 free(repo_dir); 297 free(cache_dir); 298 return append_to_dep_file(name, tarball_url); 299 } 300 301 fclose(cache_file); 302 } 303 304 fclose(repo_file); 305 } 306 307 closedir(dir); 308 free(repo_dir); 309 free(cache_dir); 310 311 return found ? 0 : -1; 312 } 313 314 static int add_from_github(const char *name, const char *requested_version) { 315 char *full_ref = NULL; 316 317 if (requested_version && requested_version[0] != '\0') { 318 full_ref = github_matching_ref(name, requested_version); 319 if (!full_ref) { 320 fprintf(stderr, "Error: ref '%s' not found for %s on GitHub\n", requested_version, name); 321 return -1; 322 } 323 free(full_ref); 324 printf("Found %s version %s on GitHub\n", name, requested_version); 325 fflush(stdout); 326 return append_to_dep_file(name, requested_version); 327 } else { 328 char *branch = github_default_branch(name); 329 if (!branch) { 330 fprintf(stderr, "Error: could not determine default branch for %s on GitHub\n", name); 331 return -1; 332 } 333 printf("Found %s on GitHub (default branch: %s)\n", name, branch); 334 fflush(stdout); 335 free(branch); 336 return append_to_dep_file(name, ""); 337 } 338 } 339 340 static int cmd_add(int argc, const char **argv) { 341 struct argparse_option options[] = { 342 OPT_HELP(), 343 OPT_END(), 344 }; 345 346 struct argparse argparse; 347 argparse_init(&argparse, options, NULL, 0); 348 argc = argparse_parse(&argparse, argc, argv); 349 350 if (argc < 1) { 351 fprintf(stderr, "Error: add requires <name> [version]\n"); 352 fprintf(stderr, "Usage: dep add <name> [version]\n"); 353 return 1; 354 } 355 356 const char *name = argv[0]; 357 const char *version = (argc > 1) ? argv[1] : NULL; 358 359 if (version && is_url(version)) { 360 printf("Adding %s as direct URL: %s\n", name, version); 361 return add_from_url(name, version); 362 } 363 364 printf("Searching repositories for %s", name); 365 if (version && strlen(version) > 0) { 366 printf(" with version %s", version); 367 } 368 printf("...\n"); 369 fflush(stdout); 370 371 int result = add_from_repository(name, version); 372 if (result == 0) { 373 printf("Added %s to .dep\n", name); 374 fflush(stdout); 375 return 0; 376 } 377 378 printf("Not found in repositories, trying GitHub...\n"); 379 fflush(stdout); 380 result = add_from_github(name, version); 381 if (result == 0) { 382 printf("Added %s to .dep\n", name); 383 return 0; 384 } 385 386 fprintf(stderr, "Error: package '%s' not found\n", name); 387 return 1; 388 } 389 390 void __attribute__((constructor)) cmd_add_setup(void) { 391 struct cmd_struct *cmd = calloc(1, sizeof(struct cmd_struct)); 392 if (!cmd) { 393 fprintf(stderr, "Failed to allocate memory for add command\n"); 394 return; 395 } 396 cmd->next = commands; 397 cmd->fn = cmd_add; 398 static const char *add_names[] = {"add", "a", NULL}; 399 cmd->name = add_names; 400 cmd->display = "a(dd)"; 401 cmd->description = "Add a new dependency to the project"; 402 cmd->help_text = 403 "dep add - Add a new dependency to the project\n" 404 "\n" 405 "Usage:\n" 406 " dep add <name>\n" 407 " dep add <name> <version>\n" 408 " dep add <name> <url>\n" 409 "\n" 410 "Description:\n" 411 " Add a package to the project's .dep file.\n" 412 "\n" 413 " If a version is not specified, the latest version from the repository\n" 414 " will be used, or the default branch from GitHub.\n" 415 "\n" 416 " You can also add a dependency with a direct URL.\n" 417 "\n" 418 "Examples:\n" 419 " dep add finwo/palloc # Latest from repo or GitHub main branch\n" 420 " dep add finwo/palloc edge # Specific version/branch\n" 421 " dep add finwo/palloc v1.0.0 # Specific tag\n" 422 " dep add mylib https://example.com/mylib.tar.gz\n"; 423 commands = cmd; 424 }