| git.druid.rocks | index | druid520 | q | src/ | q.c |
src/q.c
#include "config.h"
#ifdef OS_NETBSD_C
#define _NETBSD_SOURCE 1
#endif
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include "q.h"
static void printusage(FILE* out, const char* prog);
static void usage(const char* prog);
static void recipe_path(const struct qcfg* c, const char* arg, char* out, size_t max);
static void recipe_load(const struct qcfg* c, const char* arg, char* recipe,
size_t rmax, char* name, char* ver);
static void cmd_add(const struct qcfg* c, const char* arg);
static void cmd_del(const struct qcfg* c, const char* arg);
static void cmd_upd(const struct qcfg* c, const char* arg);
static void cmd_get(const struct qcfg* c, const char* arg);
static void cmd_list(const struct qcfg* c);
static void cmd_owns(const struct qcfg* c, const char* path);
static int cmp_kv(const void* a, const void* b);
static void
printusage(FILE* out, const char* prog)
{
fprintf(out, "usage: %s <h|a|d|u|g|l|o> [arg]\n", prog);
fprintf(out, " h show this help.\n");
fprintf(out, " a <pkg> install: run the pkg's 'add', snapshot the prefix,\n");
fprintf(out, " record the files it added in its manifest, mark it\n");
fprintf(out, " installed.\n");
fprintf(out, " d <pkg> remove: run the pkg's 'del', delete any leftover\n");
fprintf(out, " files it owns, drop it from the installed db.\n");
fprintf(out, " u <pkg> update: run the pkg's 'upd', refresh its manifest.\n");
fprintf(out, " g <pkg> fetch: run the pkg's 'get'.\n");
fprintf(out, " l list installed packages as \"name ver\".\n");
fprintf(out, " o <path> which installed pkg(s) own <path>.\n");
fprintf(out, "config: /etc/q.cfg (or $Q_CONF), a posix sh script that may set\n");
fprintf(out, "prefix, db, shell, pkgs. defaults: prefix=/usr/local,\n");
fprintf(out, "db=<prefix>/var/db/q, shell=/bin/sh, pkgs=<prefix>/pkgs.\n");
fprintf(out, "recipes: shell scripts under <pkgs>/, named <pkg>.q, with name=\"...\"\n");
fprintf(out, "ver=\"...\" headers and get/add/del/upd entrypoints.\n");
}
static void
usage(const char* prog)
{
printusage(stderr, prog);
exit(1);
}
void
badusage(const char* prog, const char* syntax)
{
fprintf(stderr, "err: bad usage.\n");
fprintf(stderr, "usage: %s %s\n", prog, syntax);
exit(1);
}
/* resolve arg (a pkg name, with or without a ".q" suffix) to a recipe
* path under <pkgs>: try <pkgs>/<arg> verbatim, then <pkgs>/<arg>.q. */
static void
recipe_path(const struct qcfg* c, const char* arg, char* out, size_t max)
{
char cand[MAXPATH];
size_t off = 0;
if(bufcat(cand, sizeof(cand), &off, c->pkgs) == 0
|| bufcat(cand, sizeof(cand), &off, "/") == 0
|| bufcat(cand, sizeof(cand), &off, arg) == 0)
{
die("path too long");
return;
}
if(access(cand, F_OK) == 0)
{
if(copystr(out, max, cand) == 0)
{
die("path too long");
return;
}
return;
}
if(bufcat(cand, sizeof(cand), &off, ".q") == 0)
{
die("path too long");
return;
}
if(access(cand, F_OK) == 0)
{
if(copystr(out, max, cand) == 0)
{
die("path too long");
return;
}
return;
}
die("no such pkg");
}
/* resolve the recipe for arg and pull its name=/ver= headers out (the
* same key="value" grammar parseconf handles). */
static void
recipe_load(const struct qcfg* c, const char* arg, char* recipe, size_t rmax,
char* name, char* ver)
{
static char buf[MAXFILE];
static struct kv kv[MAXKV];
const char* nm;
const char* vr;
long n;
long nkv;
recipe_path(c, arg, recipe, rmax);
n = readfile(recipe, buf, (long)sizeof(buf) - 1);
if(n == -2)
{
die("recipe too large to read (increase MAXFILE)");
return;
}
if(n < 0)
{
die("failed reading recipe");
return;
}
nkv = parseconf(buf, n, kv, MAXKV);
nm = kvget(kv, nkv, "name");
vr = kvget(kv, nkv, "ver");
if(nm == NULL || nm[0] == '\0')
{
die("recipe has no name");
return;
}
if(vr == NULL || vr[0] == '\0')
{
die("recipe has no ver");
return;
}
if(copystr(name, MAXKEY, nm) == 0)
{
die("pkg name too long");
return;
}
if(copystr(ver, MAXVAL, vr) == 0)
{
die("pkg version too long");
return;
}
}
static void
cmd_add(const struct qcfg* c, const char* arg)
{
char recipe[MAXPATH];
char name[MAXKEY];
char ver[MAXVAL];
struct snap before;
struct snap after;
struct snap old;
struct snap manifest;
recipe_load(c, arg, recipe, sizeof(recipe), name, ver);
if(db_has(c->db, name))
{
fprintf(stderr, "warn: %s already installed.\n", name);
}
snap_init(&before);
snap_init(&after);
snap_init(&old);
snap_init(&manifest);
walktree(&before, c->prefix, "");
snap_sort(&before);
if(runrecipe(c, recipe, "add") != 0)
{
die("pkg add failed");
return;
}
walktree(&after, c->prefix, "");
snap_sort(&after);
manifest_diff(&before, &after, &old, &manifest);
db_install(c->db, name, name, ver, &manifest);
snap_free(&before);
snap_free(&after);
snap_free(&old);
snap_free(&manifest);
}
static void
cmd_upd(const struct qcfg* c, const char* arg)
{
char recipe[MAXPATH];
char name[MAXKEY];
char ver[MAXVAL];
char pkgdir[MAXPATH];
char mpath[MAXPATH];
struct snap before;
struct snap after;
struct snap old;
struct snap manifest;
recipe_load(c, arg, recipe, sizeof(recipe), name, ver);
snap_init(&before);
snap_init(&after);
snap_init(&old);
snap_init(&manifest);
db_pkgdir(c->db, name, pkgdir, sizeof(pkgdir));
if(pathjoin(mpath, sizeof(mpath), pkgdir, "manifest") == 0)
{
die("path too long");
return;
}
/* the existing manifest, if any, is the set of paths already owned
* -- a missing one (not yet installed) is simply empty. */
if(snap_readfile(&old, mpath) >= 0)
{
snap_sort(&old);
}
walktree(&before, c->prefix, "");
snap_sort(&before);
if(runrecipe(c, recipe, "upd") != 0)
{
die("pkg upd failed");
return;
}
walktree(&after, c->prefix, "");
snap_sort(&after);
manifest_diff(&before, &after, &old, &manifest);
db_install(c->db, name, name, ver, &manifest);
snap_free(&before);
snap_free(&after);
snap_free(&old);
snap_free(&manifest);
}
static void
cmd_del(const struct qcfg* c, const char* arg)
{
char recipe[MAXPATH];
char name[MAXKEY];
char ver[MAXVAL];
char pkgdir[MAXPATH];
char mpath[MAXPATH];
struct snap manifest;
long i;
recipe_load(c, arg, recipe, sizeof(recipe), name, ver);
if(!db_has(c->db, name))
{
die("pkg not installed");
return;
}
snap_init(&manifest);
db_pkgdir(c->db, name, pkgdir, sizeof(pkgdir));
if(pathjoin(mpath, sizeof(mpath), pkgdir, "manifest") == 0)
{
die("path too long");
return;
}
if(snap_readfile(&manifest, mpath) >= 0)
{
snap_sort(&manifest);
}
if(runrecipe(c, recipe, "del") != 0)
{
die("pkg del failed");
return;
}
/* delete any manifest-listed path the recipe's own del left behind:
* files and symlinks directly, directories only if empty (a dir the
* recipe didn't clear may still hold another package's files). walk
* deepest-first so an emptied parent can be rmdir'd after its
* children. */
for(i = manifest.n - 1; i >= 0; i = i - 1)
{
const char* p = snap_path(&manifest, i);
char full[MAXPATH];
struct stat st;
if(pathjoin(full, sizeof(full), c->prefix, p) == 0)
{
die("path too long");
return;
}
if(lstat(full, &st) == 0)
{
if(S_ISDIR(st.st_mode))
{
rmdir(full);
}
else
{
unlink(full);
}
}
}
db_remove(c->db, name);
snap_free(&manifest);
}
static void
cmd_get(const struct qcfg* c, const char* arg)
{
char recipe[MAXPATH];
char name[MAXKEY];
char ver[MAXVAL];
recipe_load(c, arg, recipe, sizeof(recipe), name, ver);
if(runrecipe(c, recipe, "get") != 0)
{
die("pkg get failed");
return;
}
}
static int
cmp_kv(const void* a, const void* b)
{
const struct kv* ka = (const struct kv*)a;
const struct kv* kb = (const struct kv*)b;
return strcmp(ka->key, kb->key);
}
static void
cmd_list(const struct qcfg* c)
{
static struct kv kv[MAXKV];
long nkv;
long i;
nkv = db_list(c->db, kv, MAXKV);
qsort(kv, (size_t)nkv, sizeof(kv[0]), cmp_kv);
for(i = 0; i < nkv; i = i + 1)
{
printf("%s %s\n", kv[i].key, kv[i].val);
}
}
static void
cmd_owns(const struct qcfg* c, const char* path)
{
static struct kv kv[MAXKV];
char rel[MAXPATH];
const char* p;
size_t plen;
long nkv;
long i;
long found = 0;
/* normalize path to prefix-relative: strip the prefix if it's an
* absolute path under it, reject anything outside it. */
plen = strlen(c->prefix);
if(strncmp(path, c->prefix, plen) == 0
&& (path[plen] == '/' || path[plen] == '\0'))
{
p = path + plen;
}
else if(path[0] == '/')
{
return;
}
else
{
p = path;
}
while(*p == '/')
{
p = p + 1;
}
if(copystr(rel, sizeof(rel), p) == 0)
{
die("path too long");
return;
}
if(rel[0] == '\0')
{
return;
}
nkv = db_list(c->db, kv, MAXKV);
for(i = 0; i < nkv; i = i + 1)
{
char pkgdir[MAXPATH];
char mpath[MAXPATH];
struct snap manifest;
snap_init(&manifest);
db_pkgdir(c->db, kv[i].key, pkgdir, sizeof(pkgdir));
if(pathjoin(mpath, sizeof(mpath), pkgdir, "manifest") == 0)
{
die("path too long");
return;
}
if(snap_readfile(&manifest, mpath) >= 0)
{
snap_sort(&manifest);
if(snap_has(&manifest, rel))
{
printf("%s %s\n", kv[i].key, kv[i].val);
found = found + 1;
}
}
snap_free(&manifest);
}
if(found == 0)
{
fprintf(stderr, "not owned: %s.\n", rel);
}
}
int
main(int argc, char** argv)
{
struct qcfg cfg;
if(argc < 2)
{
usage(argv[0]);
}
loadcfg(&cfg);
if(strcmp(argv[1], "h") == 0 || strcmp(argv[1], "help") == 0)
{
printusage(stdout, argv[0]);
return 0;
}
if(strcmp(argv[1], "a") == 0)
{
if(argc != 3)
{
badusage(argv[0], "a <pkg>");
}
cmd_add(&cfg, argv[2]);
return 0;
}
if(strcmp(argv[1], "d") == 0)
{
if(argc != 3)
{
badusage(argv[0], "d <pkg>");
}
cmd_del(&cfg, argv[2]);
return 0;
}
if(strcmp(argv[1], "u") == 0)
{
if(argc != 3)
{
badusage(argv[0], "u <pkg>");
}
cmd_upd(&cfg, argv[2]);
return 0;
}
if(strcmp(argv[1], "g") == 0)
{
if(argc != 3)
{
badusage(argv[0], "g <pkg>");
}
cmd_get(&cfg, argv[2]);
return 0;
}
if(strcmp(argv[1], "l") == 0)
{
if(argc != 2)
{
badusage(argv[0], "l");
}
cmd_list(&cfg);
return 0;
}
if(strcmp(argv[1], "o") == 0)
{
if(argc != 3)
{
badusage(argv[0], "o <path>");
}
cmd_owns(&cfg, argv[2]);
return 0;
}
usage(argv[0]);
return 1;
}