/* * pkg - Repository scanner * https://rawnix.org * * Copyright (c) 2025 zorz@gmx.com * SPDX-License-Identifier: BSD-2-Clause */ #include "repo.h" #include #include #include #include #include #include /* * Repo uses a larger bucket count than the default since * a typical ports tree has hundreds of entries and we want * fast lookups during dependency resolution. */ #define REPO_BUCKETS (HASH_BUCKETS * 2) port_t *port_new(const char *name, const char *path, const char *collection) { port_t *p = xmalloc(sizeof(port_t)); p->name = xstrdup(name); p->path = xstrdup(path); p->collection = collection ? xstrdup(collection) : NULL; p->pkg = NULL; p->bad_name = false; return p; } void port_free(port_t *p) { if (!p) return; free(p->name); free(p->path); free(p->collection); if (p->pkg) package_free(p->pkg); free(p); } bool port_recipe_locked(const port_t *p) { char path[PATH_MAX]; if (!p) return false; snprintf(path, sizeof(path), "%s/MAKEPKG.lock", p->path); return access(path, F_OK) == 0; } int port_load(port_t *p) { char path[PATH_MAX]; if (!p) return -1; if (p->pkg) return 0; p->pkg = package_new(p->name, p->path); /* * MAKEPKG.lock wins, exactly as in mkpkg. `ports -u` overwrites * MAKEPKG with upstream's recipe but never the .lock, so this is * the operator's build. Every decision pkg makes from a port — * deps for prepare/add/sysup, the version pkg_is_built() looks * for, what depdb records — has to come from the recipe mkpkg * will actually build. */ snprintf(path, sizeof(path), "%s/%s", p->path, port_recipe_locked(p) ? "MAKEPKG.lock" : "MAKEPKG"); if (makepkg_parse(path, p->pkg) != 0) { package_free(p->pkg); p->pkg = NULL; return -1; } /* * mkpkg names the package after name=, pkg looks for it under the * port's directory name. They must be the same; a port where they * are not would build and then fail at install with "no package * file". Refuse it up front, and say so once. */ if (!p->pkg->name || strcmp(p->pkg->name, p->name) != 0) { if (!p->bad_name) warnx("port %s: MAKEPKG has name=%s; the port directory " "and name= must be the same", p->path, p->pkg->name ? p->pkg->name : "(none)"); p->bad_name = true; package_free(p->pkg); p->pkg = NULL; return -1; } p->pkg->loaded = true; return 0; } repo_t *repo_new(void) { repo_t *r = xmalloc(sizeof(repo_t)); size_t i; r->bucket_count = REPO_BUCKETS; r->size = 0; r->buckets = xmalloc(r->bucket_count * sizeof(repo_hash_t *)); for (i = 0; i < r->bucket_count; i++) r->buckets[i] = NULL; r->provides_buckets = NULL; r->provides_built = false; return r; } void repo_free(repo_t *r) { size_t i; repo_hash_t *e, *next; if (!r) return; if (r->provides_buckets) { provides_hash_t *p, *pnext; for (i = 0; i < REPO_BUCKETS; i++) { p = r->provides_buckets[i]; while (p) { pnext = p->next; free(p->key); strarray_free(&p->providers); free(p); p = pnext; } } free(r->provides_buckets); } for (i = 0; i < r->bucket_count; i++) { e = r->buckets[i]; while (e) { next = e->next; free(e->key); port_free(e->port); free(e); e = next; } } free(r->buckets); free(r); } static int repo_add(repo_t *r, port_t *p) { unsigned long hash; size_t idx; repo_hash_t *e, *existing; hash = hash_djb2(p->name); idx = hash % r->bucket_count; /* replace if exists (later collection wins) */ for (existing = r->buckets[idx]; existing; existing = existing->next) { if (strcmp(existing->key, p->name) == 0) { port_free(existing->port); existing->port = p; return 0; } } e = xmalloc(sizeof(repo_hash_t)); e->key = xstrdup(p->name); e->port = p; e->next = r->buckets[idx]; r->buckets[idx] = e; r->size++; return 0; } int repo_scan(repo_t *r, const char *dir, const char *collection) { DIR *d; struct dirent *ent; char path[PATH_MAX], makepkg[PATH_MAX + 16]; struct stat st; port_t *p; int count = 0; d = opendir(dir); if (!d) return -1; while ((ent = readdir(d)) != NULL) { if (ent->d_name[0] == '.') continue; snprintf(path, sizeof(path), "%s/%s", dir, ent->d_name); if (stat(path, &st) != 0 || !S_ISDIR(st.st_mode)) continue; /* a port holds a MAKEPKG, or only a MAKEPKG.lock -- the same * test as mkpkg's ports(8), which keeps a locked port whose * upstream recipe is gone */ snprintf(makepkg, sizeof(makepkg), "%s/MAKEPKG", path); if (stat(makepkg, &st) != 0 || !S_ISREG(st.st_mode)) { snprintf(makepkg, sizeof(makepkg), "%s/MAKEPKG.lock", path); if (stat(makepkg, &st) != 0 || !S_ISREG(st.st_mode)) continue; } p = port_new(ent->d_name, path, collection); if (repo_add(r, p) == 0) count++; else port_free(p); } closedir(d); return count; } int repo_scan_all(repo_t *r, char **dirs, size_t count) { size_t i; int total = 0, n; char *copy, *col; for (i = 0; i < count; i++) { copy = xstrdup(dirs[i]); col = basename(copy); n = repo_scan(r, dirs[i], col); if (n > 0) total += n; free(copy); } return total; } port_t *repo_find(repo_t *r, const char *name) { unsigned long hash; size_t idx; repo_hash_t *e; if (!r || !name) return NULL; hash = hash_djb2(name); idx = hash % r->bucket_count; for (e = r->buckets[idx]; e; e = e->next) if (strcmp(e->key, name) == 0) return e->port; return NULL; } strarray_t *repo_list(repo_t *r) { strarray_t *arr; size_t i; repo_hash_t *e; arr = xmalloc(sizeof(strarray_t)); strarray_init(arr); if (!r) return arr; for (i = 0; i < r->bucket_count; i++) for (e = r->buckets[i]; e; e = e->next) strarray_add(arr, e->key); return arr; } /* * Find variants of a package (e.g., vim-* for vim). * * A port only counts as a variant when its MAKEPKG declares a * variant=() description. Matching on the name prefix alone is not * enough: unrelated packages legitimately share a prefix (hello vs * hello-data, foo vs foo-doc), and before this check `pkg add hello` * would stop at an interactive variant prompt — hanging scripted * runs — just because a sub-package existed. */ strarray_t *repo_find_variants(repo_t *r, const char *name) { strarray_t *arr; size_t i, namelen; repo_hash_t *e; arr = xmalloc(sizeof(strarray_t)); strarray_init(arr); if (!r || !name) return arr; namelen = strlen(name); for (i = 0; i < r->bucket_count; i++) { for (e = r->buckets[i]; e; e = e->next) { /* match "name-*" pattern */ if (strncmp(e->key, name, namelen) != 0 || e->key[namelen] != '-') continue; /* ...but only ports that declare variant=() */ if (port_load(e->port) != 0) continue; if (e->port->pkg && e->port->pkg->variant && e->port->pkg->variant->size > 0) strarray_add(arr, e->key); } } return arr; } /* Check if any package provides a virtual package */ bool repo_provides(repo_t *r, const char *name) { strarray_t *prov = repo_providers(r, name); return prov != NULL && prov->size > 0; } /* ---- Provides index ---- */ static void provides_index_add(repo_t *r, const char *virt, const char *port_name) { unsigned long hash = hash_djb2(virt); size_t idx = hash % REPO_BUCKETS; provides_hash_t *e; for (e = r->provides_buckets[idx]; e; e = e->next) { if (strcmp(e->key, virt) == 0) { if (!strarray_contains(&e->providers, port_name)) strarray_add(&e->providers, port_name); return; } } e = xmalloc(sizeof(provides_hash_t)); e->key = xstrdup(virt); strarray_init(&e->providers); strarray_add(&e->providers, port_name); e->next = r->provides_buckets[idx]; r->provides_buckets[idx] = e; } /* * Build the provides index in one pass over the repo. This loads * every port (unavoidable — provides=() is only known after parsing), * but it happens at most ONCE per run. Before this index, every * dep_satisfied() / find_provider() call walked and loaded the whole * tree, allocating a full name list each time. */ static void repo_build_provides(repo_t *r) { size_t i, j; repo_hash_t *e; r->provides_buckets = xmalloc(REPO_BUCKETS * sizeof(provides_hash_t *)); for (i = 0; i < REPO_BUCKETS; i++) r->provides_buckets[i] = NULL; for (i = 0; i < r->bucket_count; i++) { for (e = r->buckets[i]; e; e = e->next) { if (port_load(e->port) != 0) continue; if (!e->port->pkg || !e->port->pkg->provides) continue; for (j = 0; j < e->port->pkg->provides->size; j++) provides_index_add(r, e->port->pkg->provides->data[j], e->port->name); } } r->provides_built = true; } strarray_t *repo_providers(repo_t *r, const char *name) { unsigned long hash; size_t idx; provides_hash_t *e; if (!r || !name) return NULL; if (!r->provides_built) repo_build_provides(r); hash = hash_djb2(name); idx = hash % REPO_BUCKETS; for (e = r->provides_buckets[idx]; e; e = e->next) if (strcmp(e->key, name) == 0) return &e->providers; return NULL; }