dep

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

commit 6f5da36e0423a6eadc64bfac67b12f5a029afdec
parent b69dc51ddd26384d6eef7a44f7303699eefccb1a
Author: finwo <finwo@pm.me>
Date:   Fri, 13 Mar 2026 15:15:28 +0100

Added install command

Diffstat:
MMakefile | 18++++++++++++++++++
Asrc/command/install/main.c | 465+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 483 insertions(+), 0 deletions(-)

diff --git a/Makefile b/Makefile @@ -16,6 +16,9 @@ LIBS:= LIBS+=lib/cofyc/argparse SRC+=lib/cofyc/argparse/argparse.c +LIBS+=lib/emmanuel-marty/em_inflate +SRC+=lib/emmanuel-marty/em_inflate/lib/em_inflate.c + LIBS+=lib/erkkah/naett SRC+=lib/erkkah/naett/naett.c ifeq ($(OS),Windows_NT) @@ -33,6 +36,9 @@ endif LIBS+=lib/rxi/microtar SRC+=lib/rxi/microtar/src/microtar.c +LIBS+=lib/tidwall/json.c +SRC+=lib/tidwall/json.c/json.c + OBJ:=$(SRC:.c=.o) OBJ:=$(OBJ:.cc=.o) OBJ+=license.o @@ -51,6 +57,12 @@ lib/cofyc/argparse: mkdir -p lib/.dep/include/cofyc ln -s ../../../cofyc/argparse/argparse.h lib/.dep/include/cofyc/argparse.h +lib/emmanuel-marty/em_inflate: + mkdir -p lib/emmanuel-marty/em_inflate + curl -sL https://github.com/emmanuel-marty/em_inflate/archive/refs/heads/master.tar.gz | tar xzv --strip-components=1 -C lib/emmanuel-marty/em_inflate + mkdir -p lib/.dep/include/emmanuel-marty + ln -s ../../../emmanuel-marty/em_inflate/lib/em_inflate.h lib/.dep/include/emmanuel-marty/em_inflate.h + lib/erkkah/naett: mkdir -p lib/erkkah/naett curl -sL https://github.com/erkkah/naett/archive/refs/heads/main.tar.gz | tar xzv --strip-components=1 -C lib/erkkah/naett @@ -63,6 +75,12 @@ lib/rxi/microtar: mkdir -p lib/.dep/include/rxi ln -s ../../../rxi/microtar/src/microtar.h lib/.dep/include/rxi/microtar.h +lib/tidwall/json.c: + mkdir -p lib/tidwall/json.c + curl -sL https://github.com/tidwall/json.c/archive/refs/heads/main.tar.gz | tar xzv --strip-components=1 -C lib/tidwall/json.c + mkdir -p lib/.dep/include/tidwall + ln -s ../../../tidwall/json.c/json.h lib/.dep/include/tidwall/json.h + .c.o: ${CC} $< ${CFLAGS} -c -o $@ diff --git a/src/command/install/main.c b/src/command/install/main.c @@ -0,0 +1,465 @@ +#include <limits.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> +#include <unistd.h> + +#include "command/command.h" +#include "emmanuel-marty/em_inflate.h" +#include "erkkah/naett.h" +#include "rxi/microtar.h" +#include "tidwall/json.h" + +typedef struct { + char *data; + size_t size; + size_t pos; +} membuffer_t; + +static int is_url(const char *str) { + return strncmp(str, "http://", 7) == 0 || strncmp(str, "https://", 8) == 0; +} + +static int dir_exists(const char *path) { + struct stat st; + return stat(path, &st) == 0 && S_ISDIR(st.st_mode); +} + +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 mem_read(mtar_t *tar, void *data, unsigned size) { + membuffer_t *buf = (membuffer_t *)tar->stream; + if (buf->pos + size > buf->size) { + return MTAR_EREADFAIL; + } + memcpy(data, buf->data + buf->pos, size); + buf->pos += size; + return MTAR_ESUCCESS; +} + +static int mem_seek(mtar_t *tar, unsigned pos) { + membuffer_t *buf = (membuffer_t *)tar->stream; + if (pos > buf->size) { + return MTAR_ESEEKFAIL; + } + buf->pos = pos; + return MTAR_ESUCCESS; +} + +static int mem_close(mtar_t *tar) { + (void)tar; + return MTAR_ESUCCESS; +} + +static int naett_initialized = 0; + +static char *download_url(const char *url, size_t *out_size); + +static char *download_url_with_retry(const char *url, size_t *out_size, int retries) { + if (!naett_initialized) { + naettInit(NULL); + naett_initialized = 1; + } + naettReq *req = naettRequest(url, naettMethod("GET"), naettTimeout(30000)); + if (!req) { + fprintf(stderr, "Error: failed to create request\n"); + return NULL; + } + + naettRes *res = naettMake(req); + if (!res) { + naettFree(req); + fprintf(stderr, "Error: failed to make request\n"); + return NULL; + } + + while (!naettComplete(res)) { + usleep(10000); + } + + int status = naettGetStatus(res); + const char *remaining = naettGetHeader(res, "X-RateLimit-Remaining"); + int body_size = 0; + const void *body = naettGetBody(res, &body_size); + + if (status == 403 || status == 429 || status == 404) { + if (remaining && strcmp(remaining, "0") == 0) { + if (retries > 0) { + fprintf(stderr, "Rate limited, waiting 5 seconds before retry...\n"); + naettClose(res); + naettFree(req); + sleep(5); + return download_url_with_retry(url, out_size, retries - 1); + } + } + } + + if (status != 200) { + fprintf(stderr, "Error: HTTP status %d for %s\n", status, url); + naettClose(res); + naettFree(req); + return NULL; + } + + if (!body || body_size == 0) { + if (retries > 0) { + fprintf(stderr, "Empty response, waiting 5 seconds before retry...\n"); + naettClose(res); + naettFree(req); + sleep(5); + return download_url_with_retry(url, out_size, retries - 1); + } + fprintf(stderr, "Error: empty response body\n"); + naettClose(res); + naettFree(req); + return NULL; + } + + char *data = malloc(body_size); + if (data) { + memcpy(data, body, body_size); + *out_size = body_size; + } + + naettClose(res); + naettFree(req); + + return data; +} + +static char *download_url(const char *url, size_t *out_size) { + return download_url_with_retry(url, out_size, 3); +} + +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 download_and_extract(const char *url, const char *dest_dir) { + size_t gzip_size; + char *gzip_data = download_url(url, &gzip_size); + if (!gzip_data) { + return -1; + } + + if (gzip_size < 10 || (unsigned char)gzip_data[0] != 0x1f || (unsigned char)gzip_data[1] != 0x8b) { + free(gzip_data); + fprintf(stderr, "Error: downloaded data is not gzip format\n"); + return -1; + } + + size_t max_tar_size = gzip_size * 10; + char *tar_data = malloc(max_tar_size); + if (!tar_data) { + free(gzip_data); + return -1; + } + + size_t tar_size = em_inflate(gzip_data, gzip_size, (unsigned char *)tar_data, max_tar_size); + free(gzip_data); + + if (tar_size == (size_t)-1 || tar_size == 0) { + free(tar_data); + fprintf(stderr, "Error: failed to decompress gzip data\n"); + return -1; + } + + membuffer_t membuf = {.data = tar_data, .size = tar_size, .pos = 0}; + + mtar_t tar; + memset(&tar, 0, sizeof(tar)); + tar.read = mem_read; + tar.seek = mem_seek; + tar.close = mem_close; + tar.stream = &membuf; + + char first_component[256] = {0}; + int first_component_found = 0; + + while (1) { + mtar_header_t h; + int err = mtar_read_header(&tar, &h); + if (err == MTAR_ENULLRECORD) break; + if (err != MTAR_ESUCCESS) { + fprintf(stderr, "Error reading tar header: %s\n", mtar_strerror(err)); + break; + } + + if (!first_component_found && (h.type == MTAR_TREG || h.type == MTAR_TDIR)) { + char *slash = strchr(h.name, '/'); + if (slash) { + size_t len = slash - h.name; + strncpy(first_component, h.name, len); + first_component[len] = '\0'; + first_component_found = 1; + } + } + + char full_path[PATH_MAX]; + char *name_ptr = h.name; + + if (first_component_found) { + size_t first_len = strlen(first_component); + if (strncmp(h.name, first_component, first_len) == 0 && h.name[first_len] == '/') { + name_ptr = h.name + first_len + 1; + } + } + + if (strlen(name_ptr) == 0) { + mtar_next(&tar); + continue; + } + + snprintf(full_path, sizeof(full_path), "%s/%s", dest_dir, name_ptr); + + if (h.type == MTAR_TDIR) { + mkdir_recursive(full_path); + } else if (h.type == MTAR_TREG) { + char *last_slash = strrchr(full_path, '/'); + if (last_slash) { + *last_slash = '\0'; + mkdir_recursive(full_path); + *last_slash = '/'; + } + + FILE *f = fopen(full_path, "wb"); + if (!f) { + fprintf(stderr, "Error: could not create file %s\n", full_path); + mtar_next(&tar); + continue; + } + + char buf[8192]; + unsigned remaining = h.size; + while (remaining > 0) { + unsigned to_read = remaining > sizeof(buf) ? sizeof(buf) : remaining; + int read_err = mtar_read_data(&tar, buf, to_read); + if (read_err != MTAR_ESUCCESS) { + fprintf(stderr, "Error reading tar data\n"); + break; + } + fwrite(buf, 1, to_read, f); + remaining -= to_read; + } + fclose(f); + } + + mtar_next(&tar); + } + + free(tar_data); + return 0; +} + +static int install_dependency(const char *name, const char *spec) { + char lib_path[PATH_MAX]; + snprintf(lib_path, sizeof(lib_path), "lib/%s", name); + + if (dir_exists(lib_path)) { + printf("Skipping %s (already installed)\n", name); + return 0; + } + + char url[2048] = {0}; + + if (strlen(spec) > 0 && is_url(spec)) { + strncpy(url, spec, sizeof(url) - 1); + } else { + char *full_ref = NULL; + + if (strlen(spec) > 0) { + full_ref = query_github_matching_ref(name, spec); + if (!full_ref) { + fprintf(stderr, "Error: ref '%s' not found for %s\n", spec, name); + return -1; + } + } else { + char *branch = query_github_default_branch(name); + if (!branch) { + fprintf(stderr, "Warning: could not determine default branch for %s, using 'main'\n", name); + branch = strdup("main"); + } + full_ref = malloc(256); + if (full_ref) { + snprintf(full_ref, 256, "refs/heads/%s", branch); + } + free(branch); + } + + if (!full_ref) { + fprintf(stderr, "Error: could not determine ref for %s\n", name); + return -1; + } + + snprintf(url, sizeof(url), "https://github.com/%s/archive/%s.tar.gz", name, full_ref); + free(full_ref); + } + + printf("Installing %s from %s\n", name, url); + + mkdir_recursive(lib_path); + + if (download_and_extract(url, lib_path) != 0) { + fprintf(stderr, "Error: failed to install %s\n", name); + return -1; + } + + printf("Installed %s\n", name); + return 0; +} + +static int cmd_install(int argc, const char **argv) { + (void)argc; + (void)argv; + + const char *dep_path = ".dep"; + FILE *f = fopen(dep_path, "r"); + if (!f) { + fprintf(stderr, "Error: .dep file not found. Run 'dep init' first.\n"); + return 1; + } + + if (!dir_exists("lib")) { + if (mkdir("lib", 0755) != 0) { + fprintf(stderr, "Error: could not create lib directory\n"); + fclose(f); + return 1; + } + } + + char line[LINE_MAX]; + int has_deps = 0; + + while (fgets(line, sizeof(line), f)) { + char *comment = strchr(line, '#'); + if (comment) *comment = '\0'; + + char *trimmed = trim_whitespace(line); + if (strlen(trimmed) == 0) continue; + + has_deps = 1; + + char name[256] = {0}; + char spec[1024] = {0}; + + char *at_pos = strchr(trimmed, '@'); + if (at_pos) { + size_t name_len = at_pos - trimmed; + strncpy(name, trimmed, name_len); + name[name_len] = '\0'; + strcpy(spec, trim_whitespace(at_pos + 1)); + } else { + strncpy(name, trimmed, sizeof(name) - 1); + name[sizeof(name) - 1] = '\0'; + } + + name[strcspn(name, " \t")] = '\0'; + + if (strlen(name) == 0) continue; + + install_dependency(name, spec); + } + + fclose(f); + + if (!has_deps) { + printf("No dependencies to install\n"); + } + + return 0; +} + +void __attribute__((constructor)) cmd_install_setup(void) { + struct cmd_struct *cmd = calloc(1, sizeof(struct cmd_struct)); + if (!cmd) { + fprintf(stderr, "Failed to allocate memory for install command\n"); + return; + } + cmd->next = commands; + cmd->fn = cmd_install; + static const char *install_names[] = {"install", "i", NULL}; + cmd->name = install_names; + commands = cmd; +}