dep

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

commit 4d6f40fb6d1833db6f5311768432f45b578c1dec
parent d90175272f7ff91cdeb67c261649edd9103b3a7b
Author: finwo <finwo@pm.me>
Date:   Fri, 13 Mar 2026 23:46:20 +0100

Added 'add' command

Diffstat:
Asrc/command/add/main.c | 462+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/command/install/main.c | 65++++-------------------------------------------------------------
Asrc/common/github-utils.c | 63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/common/github-utils.h | 11+++++++++++
4 files changed, 540 insertions(+), 61 deletions(-)

diff --git a/src/command/add/main.c b/src/command/add/main.c @@ -0,0 +1,462 @@ +#include <dirent.h> +#include <limits.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> +#include <time.h> +#include <unistd.h> + +#include "cofyc/argparse.h" +#include "command/command.h" +#include "common/github-utils.h" +#include "common/net-utils.h" + +#define REPO_DIR_DEFAULT "/.config/finwo/dep/repositories.d/" +#define CACHE_DIR_DEFAULT "/.config/finwo/dep/repositories.cache/" +#define CACHE_MAX_AGE_SECONDS (7 * 24 * 60 * 60) + +static char *get_repo_dir(void) { + const char *home = getenv("HOME"); + if (!home) { + fprintf(stderr, "Error: HOME environment variable not set\n"); + return NULL; + } + size_t len = strlen(home) + strlen(REPO_DIR_DEFAULT) + 1; + char *path = malloc(len); + if (!path) { + fprintf(stderr, "Error: out of memory\n"); + return NULL; + } + snprintf(path, len, "%s%s", home, REPO_DIR_DEFAULT); + return path; +} + +static char *get_cache_dir(void) { + const char *home = getenv("HOME"); + if (!home) { + fprintf(stderr, "Error: HOME environment variable not set\n"); + return NULL; + } + size_t len = strlen(home) + strlen(CACHE_DIR_DEFAULT) + 1; + char *path = malloc(len); + if (!path) { + fprintf(stderr, "Error: out of memory\n"); + return NULL; + } + snprintf(path, len, "%s%s", home, CACHE_DIR_DEFAULT); + return path; +} + +static unsigned long hash_string(const char *str) { + unsigned long hash = 5381; + int c; + while ((c = *str++)) { + hash = ((hash << 5) + hash) + c; + } + return hash; +} + +static void url_to_cache_filename(const char *url, char *filename, size_t size) { + unsigned long hash = hash_string(url); + snprintf(filename, size, "%lu", hash); +} + +static int is_cache_outdated(const char *cache_path) { + struct stat st; + if (stat(cache_path, &st) != 0) { + return 1; + } + time_t now = time(NULL); + time_t file_age = now - st.st_mtime; + return file_age > CACHE_MAX_AGE_SECONDS; +} + +static int mkdir_recursive(const char *path) { + char tmp[PATH_MAX]; + char *p = NULL; + size_t len; + + snprintf(tmp, sizeof(tmp), "%s", path); + len = strlen(tmp); + if (tmp[len - 1] == '/') { + tmp[len - 1] = '\0'; + } + + for (p = tmp + 1; *p; p++) { + if (*p == '/') { + *p = '\0'; + mkdir(tmp, 0755); + *p = '/'; + } + } + return mkdir(tmp, 0755); +} + +static char *trim_whitespace(char *str) { + while (*str == ' ' || *str == '\t' || *str == '\n' || *str == '\r') str++; + if (*str == '\0') return str; + char *end = str + strlen(str) - 1; + while (end > str && (*end == ' ' || *end == '\t' || *end == '\n' || *end == '\r')) { + *end = '\0'; + end--; + } + return str; +} + +static int extract_version_from_depname(const char *depname_with_version, char *depname, char *version, + size_t depname_size, size_t version_size) { + const char *at_pos = strchr(depname_with_version, '@'); + if (!at_pos) { + strncpy(depname, depname_with_version, depname_size - 1); + depname[depname_size - 1] = '\0'; + version[0] = '\0'; + return 0; + } + + size_t depname_len = at_pos - depname_with_version; + if (depname_len >= depname_size) { + depname_len = depname_size - 1; + } + strncpy(depname, depname_with_version, depname_len); + depname[depname_len] = '\0'; + + strncpy(version, at_pos + 1, version_size - 1); + version[version_size - 1] = '\0'; + return 0; +} + +static int parse_manifest_line(const char *line, char *depname, char *version, char *url, size_t depname_size, + size_t version_size, size_t url_size) { + char *line_copy = strdup(line); + if (!line_copy) return -1; + + char *comment = strchr(line_copy, '#'); + if (comment) *comment = '\0'; + + char *trimmed = trim_whitespace(line_copy); + if (strlen(trimmed) == 0) { + free(line_copy); + return -1; + } + + char *space_pos = strchr(trimmed, ' '); + char *tab_pos = strchr(trimmed, '\t'); + char *first_ws = NULL; + + if (space_pos && tab_pos) { + first_ws = (space_pos < tab_pos) ? space_pos : tab_pos; + } else if (space_pos) { + first_ws = space_pos; + } else if (tab_pos) { + first_ws = tab_pos; + } + + int result = 0; + + if (first_ws) { + size_t name_len = first_ws - trimmed; + if (name_len >= depname_size) { + name_len = depname_size - 1; + } + strncpy(depname, trimmed, name_len); + depname[name_len] = '\0'; + + char *url_start = trim_whitespace(first_ws + 1); + strncpy(url, url_start, url_size - 1); + url[url_size - 1] = '\0'; + } else { + strncpy(depname, trimmed, depname_size - 1); + depname[depname_size - 1] = '\0'; + url[0] = '\0'; + } + + char version_from_depname[256]; + extract_version_from_depname(depname, depname, version_from_depname, depname_size, sizeof(version_from_depname)); + + if (version_from_depname[0] != '\0') { + strncpy(version, version_from_depname, version_size - 1); + version[version_size - 1] = '\0'; + } else { + version[0] = '\0'; + } + + free(line_copy); + return result; +} + +static int version_matches(const char *requested, const char *available) { + if (!requested || requested[0] == '\0') { + return 1; + } + if (!available || available[0] == '\0') { + return 0; + } + return strcmp(requested, available) == 0; +} + +static int append_to_dep_file(const char *name, const char *spec) { + const char *dep_path = ".dep"; + FILE *f = fopen(dep_path, "a"); + if (!f) { + fprintf(stderr, "Error: could not open .dep file for writing\n"); + return -1; + } + + fseek(f, 0, SEEK_END); + long pos = ftell(f); + if (pos > 0) { + fputc('\n', f); + } + + if (spec && spec[0] != '\0') { + fprintf(f, "%s %s\n", name, spec); + } else { + fprintf(f, "%s\n", name); + } + + fclose(f); + return 0; +} + +static int add_from_url(const char *name, const char *url) { + int result = append_to_dep_file(name, url); + if (result == 0) { + printf("Added %s to .dep\n", name); + } + return result; +} + +static int add_from_repository(const char *name, const char *requested_version) { + char *repo_dir = get_repo_dir(); + if (!repo_dir) { + return -1; + } + + char *cache_dir = get_cache_dir(); + if (!cache_dir) { + free(repo_dir); + return -1; + } + + mkdir_recursive(cache_dir); + + DIR *dir = opendir(repo_dir); + if (!dir) { + fprintf(stderr, "Error: could not open repository directory\n"); + free(repo_dir); + free(cache_dir); + return -1; + } + + struct dirent *entry; + int found = 0; + + while (!found && (entry = readdir(dir)) != NULL) { + if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, "..")) continue; + + char *repo_dir2 = get_repo_dir(); + if (!repo_dir2) continue; + + char filepath[PATH_MAX]; + snprintf(filepath, sizeof(filepath), "%s%s", repo_dir2, entry->d_name); + free(repo_dir2); + + struct stat st; + if (stat(filepath, &st) < 0 || !S_ISREG(st.st_mode)) continue; + + FILE *repo_file = fopen(filepath, "r"); + if (!repo_file) continue; + + char line[LINE_MAX]; + while (!found && fgets(line, sizeof(line), repo_file)) { + char *comment = strchr(line, '#'); + if (comment) *comment = '\0'; + + char *trimmed = trim_whitespace(line); + if (strlen(trimmed) == 0) continue; + + char repo_name[256] = {0}; + char manifest_url[1024] = {0}; + + char *space_pos = strchr(trimmed, ' '); + char *tab_pos = strchr(trimmed, '\t'); + char *first_ws = NULL; + + if (space_pos && tab_pos) { + first_ws = (space_pos < tab_pos) ? space_pos : tab_pos; + } else if (space_pos) { + first_ws = space_pos; + } else if (tab_pos) { + first_ws = tab_pos; + } + + if (!first_ws) continue; + + size_t name_len = first_ws - trimmed; + if (name_len >= sizeof(repo_name)) { + name_len = sizeof(repo_name) - 1; + } + strncpy(repo_name, trimmed, name_len); + repo_name[name_len] = '\0'; + + char *url_start = trim_whitespace(first_ws + 1); + strncpy(manifest_url, url_start, sizeof(manifest_url) - 1); + manifest_url[sizeof(manifest_url) - 1] = '\0'; + + if (strlen(manifest_url) == 0) continue; + + char cache_filename[256]; + url_to_cache_filename(manifest_url, cache_filename, sizeof(cache_filename)); + + char cache_path[PATH_MAX]; + snprintf(cache_path, sizeof(cache_path), "%s%s", cache_dir, cache_filename); + + if (is_cache_outdated(cache_path)) { + printf("Updating cache for repository %s...\n", repo_name); + size_t size; + char *manifest_content = download_url(manifest_url, &size); + if (manifest_content) { + FILE *cache_file = fopen(cache_path, "w"); + if (cache_file) { + fwrite(manifest_content, 1, size, cache_file); + fclose(cache_file); + } + free(manifest_content); + } + } + + FILE *cache_file = fopen(cache_path, "r"); + if (!cache_file) continue; + + char manifest_line[LINE_MAX]; + while (!found && fgets(manifest_line, sizeof(manifest_line), cache_file)) { + char depname[256] = {0}; + char version[256] = {0}; + char tarball_url[1024] = {0}; + + if (parse_manifest_line(manifest_line, depname, version, tarball_url, sizeof(depname), sizeof(version), + sizeof(tarball_url)) != 0) { + continue; + } + + if (strcmp(depname, name) != 0) continue; + + if (!version_matches(requested_version, version)) continue; + + if (strlen(tarball_url) == 0) continue; + + printf("Found %s", name); + if (version[0] != '\0') { + printf(" version %s", version); + } + printf(" in repository %s\n", repo_name); + + fclose(cache_file); + closedir(dir); + free(repo_dir); + free(cache_dir); + return append_to_dep_file(name, tarball_url); + } + + fclose(cache_file); + } + + fclose(repo_file); + } + + closedir(dir); + free(repo_dir); + free(cache_dir); + + return found ? 0 : -1; +} + +static int add_from_github(const char *name, const char *requested_version) { + char *full_ref = NULL; + + if (requested_version && requested_version[0] != '\0') { + full_ref = github_matching_ref(name, requested_version); + if (!full_ref) { + fprintf(stderr, "Error: ref '%s' not found for %s on GitHub\n", requested_version, name); + return -1; + } + free(full_ref); + printf("Found %s version %s on GitHub\n", name, requested_version); + fflush(stdout); + return append_to_dep_file(name, requested_version); + } else { + char *branch = github_default_branch(name); + if (!branch) { + fprintf(stderr, "Error: could not determine default branch for %s on GitHub\n", name); + return -1; + } + printf("Found %s on GitHub (default branch: %s)\n", name, branch); + fflush(stdout); + free(branch); + return append_to_dep_file(name, ""); + } +} + +static int cmd_add(int argc, const char **argv) { + struct argparse_option options[] = { + OPT_HELP(), + OPT_END(), + }; + + struct argparse argparse; + argparse_init(&argparse, options, NULL, 0); + argc = argparse_parse(&argparse, argc, argv); + + if (argc < 1) { + fprintf(stderr, "Error: add requires <name> [version]\n"); + fprintf(stderr, "Usage: dep add <name> [version]\n"); + return 1; + } + + const char *name = argv[0]; + const char *version = (argc > 1) ? argv[1] : NULL; + + if (version && is_url(version)) { + printf("Adding %s as direct URL: %s\n", name, version); + return add_from_url(name, version); + } + + printf("Searching repositories for %s", name); + if (version && strlen(version) > 0) { + printf(" with version %s", version); + } + printf("...\n"); + fflush(stdout); + + int result = add_from_repository(name, version); + if (result == 0) { + printf("Added %s to .dep\n", name); + fflush(stdout); + return 0; + } + + printf("Not found in repositories, trying GitHub...\n"); + fflush(stdout); + result = add_from_github(name, version); + if (result == 0) { + printf("Added %s to .dep\n", name); + return 0; + } + + fprintf(stderr, "Error: package '%s' not found\n", name); + return 1; +} + +void __attribute__((constructor)) cmd_add_setup(void) { + struct cmd_struct *cmd = calloc(1, sizeof(struct cmd_struct)); + if (!cmd) { + fprintf(stderr, "Failed to allocate memory for add command\n"); + return; + } + cmd->next = commands; + cmd->fn = cmd_add; + static const char *add_names[] = {"add", NULL}; + cmd->name = add_names; + commands = cmd; +} diff --git a/src/command/install/main.c b/src/command/install/main.c @@ -6,16 +6,14 @@ #include <unistd.h> #include "command/command.h" +#include "common/github-utils.h" #include "common/net-utils.h" #include "emmanuel-marty/em_inflate.h" #include "erkkah/naett.h" #include "rxi/microtar.h" -#include "tidwall/json.h" /* Forward declarations */ -static char *query_github_default_branch(const char *full_name); -static char *query_github_matching_ref(const char *full_name, const char *ref); -static int install_dependency(const char *name, const char *spec); +static int install_dependency(const char *name, const char *spec); static int dir_exists(const char *path) { struct stat st; @@ -62,13 +60,13 @@ static char *spec_to_url(const char *name, const char *spec) { char *full_ref = NULL; if (strlen(spec) > 0) { - full_ref = query_github_matching_ref(name, spec); + full_ref = github_matching_ref(name, spec); if (!full_ref) { fprintf(stderr, "Error: ref '%s' not found for %s\n", spec, name); return NULL; } } else { - char *branch = query_github_default_branch(name); + char *branch = github_default_branch(name); if (!branch) { fprintf(stderr, "Warning: could not determine default branch for %s, using 'main'\n", name); branch = strdup("main"); @@ -145,61 +143,6 @@ static int process_dep_file_in_dir(const char *dep_dir) { return 0; } -static char *query_github_default_branch(const char *full_name) { - char url[512]; - snprintf(url, sizeof(url), "https://api.github.com/repos/%s", full_name); - - size_t size; - char *response = download_url(url, &size); - if (!response) return NULL; - - struct json root = json_parse(response); - struct json default_branch = json_object_get(root, "default_branch"); - - char *branch = NULL; - if (json_exists(default_branch) && json_type(default_branch) == JSON_STRING) { - size_t len = json_string_length(default_branch); - branch = malloc(len + 1); - if (branch) { - json_string_copy(default_branch, branch, len + 1); - } - } - - free(response); - return branch; -} - -static char *query_github_ref(const char *full_name, const char *ref_type, const char *ref) { - char url[512]; - snprintf(url, sizeof(url), "https://api.github.com/repos/%s/git/ref/%s/%s", full_name, ref_type, ref); - - size_t size; - char *response = download_url(url, &size); - if (!response) return NULL; - - struct json root = json_parse(response); - struct json ref_obj = json_object_get(root, "ref"); - - char *full_ref = NULL; - if (json_exists(ref_obj) && json_type(ref_obj) == JSON_STRING) { - size_t len = json_string_length(ref_obj); - full_ref = malloc(len + 1); - if (full_ref) { - json_string_copy(ref_obj, full_ref, len + 1); - } - } - - free(response); - return full_ref; -} - -static char *query_github_matching_ref(const char *full_name, const char *ref) { - char *full_ref = query_github_ref(full_name, "tags", ref); - if (full_ref) return full_ref; - - return query_github_ref(full_name, "heads", ref); -} - static int install_dependency(const char *name, const char *spec) { char lib_path[PATH_MAX]; snprintf(lib_path, sizeof(lib_path), "lib/%s", name); diff --git a/src/common/github-utils.c b/src/common/github-utils.c @@ -0,0 +1,63 @@ +#include "github-utils.h" + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include "net-utils.h" +#include "tidwall/json.h" + +static char *github_ref(const char *full_name, const char *ref_type, const char *ref) { + char url[512]; + snprintf(url, sizeof(url), "https://api.github.com/repos/%s/git/ref/%s/%s", full_name, ref_type, ref); + + size_t size; + char *response = download_url(url, &size); + if (!response) return NULL; + + struct json root = json_parse(response); + struct json ref_obj = json_object_get(root, "ref"); + + char *full_ref = NULL; + if (json_exists(ref_obj) && json_type(ref_obj) == JSON_STRING) { + size_t len = json_string_length(ref_obj); + full_ref = malloc(len + 1); + if (full_ref) { + json_string_copy(ref_obj, full_ref, len + 1); + } + } + + free(response); + return full_ref; +} + +char *github_default_branch(const char *full_name) { + char url[512]; + snprintf(url, sizeof(url), "https://api.github.com/repos/%s", full_name); + + size_t size; + char *response = download_url(url, &size); + if (!response) return NULL; + + struct json root = json_parse(response); + struct json default_branch = json_object_get(root, "default_branch"); + + char *branch = NULL; + if (json_exists(default_branch) && json_type(default_branch) == JSON_STRING) { + size_t len = json_string_length(default_branch); + branch = malloc(len + 1); + if (branch) { + json_string_copy(default_branch, branch, len + 1); + } + } + + free(response); + return branch; +} + +char *github_matching_ref(const char *full_name, const char *ref) { + char *full_ref = github_ref(full_name, "tags", ref); + if (full_ref) return full_ref; + + return github_ref(full_name, "heads", ref); +} diff --git a/src/common/github-utils.h b/src/common/github-utils.h @@ -0,0 +1,11 @@ +#ifndef GITHUB_UTILS_H +#define GITHUB_UTILS_H + +#include <stddef.h> + +#include "net-utils.h" + +char *github_default_branch(const char *full_name); +char *github_matching_ref(const char *full_name, const char *ref); + +#endif // GITHUB_UTILS_H