文件
ParlzPackageManger/src/main.c
T
JGZYES f5f1166bde feature: pmm update refreshes registry index (apt-style)
pmm update is no longer an alias of self-update. It now downloads packages.json
plus every {pkg}.json from the configured mirrors into ~/.pmm/cache/registry/
so subsequent search/info/upgrade can consult them offline.

- Add cmd_update() + save_registry_cache().
- Routed 'update' to cmd_update() (self-update stays self-update; upgrade stays
  upgrade).
- Add PMM_MKDIR macro for cross-platform mkdir in main.c.
- help: distinguish self-update (tool), update (refresh index), upgrade (packages).

Verified on WSL: 'pmm update' caches 6/6 packages into ~/.pmm/cache/registry/.
2026-09-04 20:37:36 +08:00

927 行
38 KiB
C

/* main.c - ParlzPackageManger (PMM)
*
* Usage:
* pmm install <pkg> install from configured registry
* pmm install --git <owner/repo> install latest release asset for this OS
* pmm install --git <url> full URL: github/gitlab/gitea/forgejo
* pmm install --host <host> --git <slug> force host (github|gitlab|gitea|forgejo)
* pmm mirror list list configured mirrors
* pmm mirror add <name> <api-url> add a mirror (writes mirror.ini)
* pmm mirror remove <name> remove a mirror
* pmm mirror use <name> set active mirror
* pmm list list installed packages
* pmm version | help
*
* Config: ~/.pmm/pmm.json | pmm.ini | pmm.conf (first found wins)
* Mirrors: ~/.pmm/mirror.ini | mirror.conf
*/
#include <stdio.h>
#ifdef _WIN32
#include <windows.h>
#else
#include <dirent.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#endif
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include "pmm.h"
#include "out.h"
#include "json.h"
#include "ini.h"
#include "http.h"
#include "repo.h"
#include "install.h"
#include "pdm.h"
#include "mirrors.h"
#include "sha256.h"
#include "sha1.h"
#if defined(_WIN32)
#include <direct.h>
#define PMM_MKDIR(p) _mkdir(p)
#else
#define PMM_MKDIR(p) mkdir((p), 0755)
#endif
typedef struct {
char *registry_url; /* package registry base URL */
char *mirror_name; /* active mirror name ("" = none) */
} PmmConfig;
typedef struct {
char *name; /* active mirror name or NULL */
char *api_base; /* mirror API base, NULL = use default host URLs */
} MirrorSel;
static char *xstrdup(const char *s) { return s ? strdup(s) : NULL; }
/* ---------- config loading (json / ini / conf) ---------- */
static void load_config(PmmConfig *cfg) {
char dir[1024];
pmm_config_dir(dir, sizeof(dir));
memset(cfg, 0, sizeof(*cfg));
char *path = pmm_find_config(dir, "pmm");
if (!path) return;
if (strstr(path, ".json")) {
JsonValue *root = json_parse_file(path);
if (root) {
const char *u = json_str(root, "registry");
if (u) cfg->registry_url = xstrdup(u);
const char *m = json_str(root, "mirror");
if (m) cfg->mirror_name = xstrdup(m);
json_free(root);
}
} else {
Ini *ini = ini_load(path);
if (ini) {
const char *u = ini_get(ini, NULL, "registry");
if (!u) u = ini_get(ini, "pmm", "registry");
if (u) cfg->registry_url = xstrdup(u);
const char *m = ini_get(ini, NULL, "mirror");
if (!m) m = ini_get(ini, "pmm", "mirror");
if (m) cfg->mirror_name = xstrdup(m);
ini_free(ini);
}
}
}
static const char *mirror_section_name(void) {
#if defined(_WIN32)
return pmm_os_name(pmm_detect_os()); /* per-OS section falls back below */
#else
return pmm_os_name(pmm_detect_os());
#endif
}
/* Load mirrors from mirror.ini / mirror.conf; pick active mirror (by cfg
* name, first "default=true", or first entry). Format:
* [name]
* api = https://mirror.example.com (rewrites github api; or gitea api base)
* default = true|false
*/
static void load_mirror(PmmConfig *cfg, MirrorSel *sel) {
char dir[1024];
pmm_config_dir(dir, sizeof(dir));
pmm_ensure_default_mirror(dir);
memset(sel, 0, sizeof(*sel));
char *path = pmm_find_config(dir, "mirror");
if (!path) return;
Ini *ini = ini_load(path);
if (!ini) return;
/* discover section names (order of appearance) */
const char *first = NULL, *chosen = NULL;
for (IniEntry *e = ini->head; e; e = e->next) {
if (!*e->section) continue;
if (!first) first = e->section;
const char *dflt = ini_get(ini, e->section, "default");
if (dflt && (strcmp(dflt, "true") == 0 || strcmp(dflt, "1") == 0) && !chosen)
chosen = e->section;
}
const char *want = (cfg->mirror_name && *cfg->mirror_name) ? cfg->mirror_name : chosen;
if (!want) want = first;
if (!want) { ini_free(ini); return; }
sel->name = xstrdup(want);
const char *api = ini_get(ini, want, "api");
if (!api) api = ini_get(ini, mirror_section_name(), "api");
sel->api_base = xstrdup(api);
ini_free(ini);
}
/* ---------- commands ---------- */
static const char *host_name(PmmHost h) {
switch (h) {
case HOST_GITHUB: return "github";
case HOST_GITLAB: return "gitlab";
case HOST_GITEA: return "gitea";
case HOST_FORGEJO: return "forgejo";
default: return "auto";
}
}
static PmmHost host_from_name(const char *h) {
if (strcmp(h, "github") == 0) return HOST_GITHUB;
if (strcmp(h, "gitlab") == 0) return HOST_GITLAB;
if (strcmp(h, "gitea") == 0) return HOST_GITEA;
if (strcmp(h, "forgejo") == 0) return HOST_FORGEJO;
return HOST_UNKNOWN;
}
/* True if the filename has a known auto-installable extension (local packages). */
static int has_installer_ext(const char *s) {
static const char *exts[] = { ".deb", ".rpm", ".appimage", ".msi", ".exe", ".zip",
".tgz", ".tar.gz", ".tar.xz", ".tar.bz2", ".7z",
".dmg", ".pkg", ".pkg.tar.zst", NULL };
size_t l = strlen(s);
for (int i = 0; exts[i]; i++) {
size_t el = strlen(exts[i]);
if (l >= el && strcmp(s + l - el, exts[i]) == 0) return 1;
}
return 0;
}
/* Case-insensitive substring (avoids non-standard strcasestr). */
static int ci_contains(const char *hay, const char *needle) {
size_t nl = strlen(needle);
if (!nl) return 1;
for (; *hay; hay++) {
size_t i;
for (i = 0; i < nl && hay[i]; i++)
if (tolower((unsigned char)hay[i]) != tolower((unsigned char)needle[i])) break;
if (i == nl) return 1;
}
return 0;
}
/* Fetch `{registry}/{rel}` from the configured mirrors (by priority).
* Returns malloc'd body or NULL; *outstatus receives the HTTP code. */
static char *registry_fetch(const char *rel, int *outstatus) {
MirrorList *ml = mirrors_load();
char *bases[128]; int nb = 0;
for (int i = 0; i < ml->count && nb < 128; i++)
if (ml->items[i].registry && *ml->items[i].registry)
bases[nb++] = ml->items[i].registry;
char url[2048]; int status = 0; char *body = NULL;
for (int i = 0; i < nb; i++) {
snprintf(url, sizeof(url), "%s/%s", bases[i], rel);
body = http_get(url, &status);
if (body && status != 404 && status != 403 && status != 503) break;
free(body); body = NULL;
}
mirrors_free(ml);
if (outstatus) *outstatus = status;
return body;
}
/* pmm search <keyword> — list registry packages matching keyword. */
static int cmd_search(int argc, char **argv) {
if (argc < 1) { pmm_error("usage: pmm search <keyword>\n"); return 1; }
int status = 0;
char *body = registry_fetch("packages.json", &status);
if (!body) { pmm_error("no registry index (packages.json) available\n"); return 1; }
JsonValue *root = json_parse(body);
free(body);
if (!root || root->type != JSON_ARRAY) { pmm_error("bad registry index\n"); json_free(root); return 1; }
const char *kw = argv[0];
int found = 0;
for (int i = 0; i < root->count; i++) {
JsonValue *v = json_at(root, i);
const char *name = (v && v->type == JSON_STRING) ? v->string : NULL;
if (!name) continue;
if (ci_contains(name, kw)) { printf(" %s\n", name); found++; }
}
json_free(root);
if (!found) pmm_info("no package matches '%s'\n", kw);
return 0;
}
/* pmm info <package|file.pdm> — registry package info, or a local .pdm's control. */
static int cmd_info(int argc, char **argv) {
if (argc < 1) { pmm_error("usage: pmm info <package|file.pdm>\n"); return 1; }
const char *pkg = argv[0];
/* local .pdm -> control info */
FILE *chk = fopen(pkg, "rb");
if (chk) {
fclose(chk);
size_t lp = strlen(pkg);
if (lp > 4 && (strcmp(pkg + lp - 4, ".pdm") == 0 || strcmp(pkg + lp - 4, ".PDM") == 0))
return pdm_info(pkg);
}
/* registry package */
char rel[512]; snprintf(rel, sizeof(rel), "%s.json", pkg);
int status = 0;
char *body = registry_fetch(rel, &status);
if (!body) { pmm_error("package '%s' not found in registry\n", pkg); return 1; }
JsonValue *root = json_parse(body);
free(body);
if (!root || root->type != JSON_OBJECT) { pmm_error("bad registry entry '%s'\n", pkg); json_free(root); return 1; }
const char *name = json_str(root, "name");
const char *ver = json_str(root, "version");
pmm_info("%s%s%s\n", name ? name : pkg, ver ? " " : "", ver ? ver : "");
const char *desc = json_str(root, "description");
if (desc) pmm_info("description: %s\n", desc);
JsonValue *vr = json_get(root, "variants");
if (vr && vr->count > 0) {
printf(" versions:\n");
for (int i = 0; i < vr->count; i++) {
JsonValue *v = json_at(vr, i);
if (!v) continue;
const char *vv = json_str(v, "version");
const char *osn = json_str(v, "os");
const char *archn = json_str(v, "arch");
printf(" %-12s %s/%s\n", vv ? vv : "?", osn ? osn : "-", archn ? archn : "-");
}
} else {
const char *u = json_str(root, "url");
const char *s256 = json_str(root, "sha256");
printf(" url: %s\n", u ? u : "-");
if (s256) printf(" sha256: %s\n", s256);
}
json_free(root);
return 0;
}
/* Compare two dotted versions ("1.0.51" vs "1.0.5"), numeric component-wise.
* Returns >0 if a is newer, <0 if b newer, 0 if equal. Mirrors install.c vcmp. */
static int cmp_version(const char *a, const char *b) {
const char *pa = a, *pb = b;
while (*pa || *pb) {
double va = 0, vb = 0; int ha = 0, hb = 0;
while (*pa && *pa != '.' && *pa != '-') { va = va * 10 + (*pa - '0'); pa++; ha = 1; }
while (*pb && *pb != '.' && *pb != '-') { vb = vb * 10 + (*pb - '0'); pb++; hb = 1; }
if (ha != hb) return ha < hb ? -1 : 1;
if (va != vb) return va < vb ? -1 : 1;
if (*pa == '.') pa++; if (*pb == '.') pb++;
}
return 0;
}
/* Read "Package:" and "Version:" from the first lines of an installed .info.
* The header block is authoritative (the embedded ctl repeats them lower). */
static void installed_version(const char *info, char *pkg, size_t pkgsz, char *ver, size_t versz) {
const char *p = info;
pkg[0] = ver[0] = '\0';
while (p && *p) {
const char *eol = strchr(p, '\n');
size_t ll = eol ? (size_t)(eol - p) : strlen(p);
char line[512];
if (ll >= sizeof(line)) ll = sizeof(line) - 1;
memcpy(line, p, ll); line[ll] = '\0';
char *colon = strchr(line, ':');
if (colon) {
*colon = '\0';
const char *key = line, *val = colon + 1;
while (*val == ' ' || *val == '\t') val++;
char *e = val + strlen(val);
while (e > val && (e[-1] == ' ' || e[-1] == '\r')) *--e = '\0';
if (strcmp(key, "Package") == 0) snprintf(pkg, pkgsz, "%s", val);
else if (strcmp(key, "Version") == 0) snprintf(ver, versz, "%s", val);
}
if (!eol) break;
p = eol + 1;
}
}
/* pmm upgrade — for each installed .pdm, look up the registry latest version
* and offer to upgrade. With --yes, upgrade without prompting. */
static int cmd_upgrade(int argc, char **argv) {
int yes = 0;
for (int i = 0; i < argc; i++)
if (strcmp(argv[i], "--yes") == 0 || strcmp(argv[i], "-y") == 0) yes = 1;
char home[1024];
pmm_config_dir(home, sizeof(home));
char db[1200];
snprintf(db, sizeof(db), "%s/installed", home);
DIR *d = opendir(db);
if (!d) { pmm_info("no installed packages.\n"); return 0; }
PmmConfig cfg; MirrorSel mirror;
load_config(&cfg); load_mirror(&cfg, &mirror);
struct dirent *e;
int upgraded = 0, checked = 0;
while ((e = readdir(d)) != NULL) {
size_t ln = strlen(e->d_name);
if (ln < 5 || strcmp(e->d_name + ln - 5, ".info") != 0) continue;
char ipath[1400], info[4096];
snprintf(ipath, sizeof(ipath), "%s/%s", db, e->d_name);
FILE *fp = fopen(ipath, "rb");
if (!fp) continue;
size_t got = fread(info, 1, sizeof(info) - 1, fp);
fclose(fp);
info[got] = '\0';
char pkg[256], curver[128];
installed_version(info, pkg, sizeof(pkg), curver, sizeof(curver));
if (!pkg[0]) continue;
checked++;
/* fetch latest for this package (current os/arch) */
char rel[512]; snprintf(rel, sizeof(rel), "%s.json", pkg);
int status = 0;
char *body = registry_fetch(rel, &status);
if (!body) { pmm_warn("skipping %s: no registry entry\n", pkg); continue; }
JsonValue *root = json_parse(body);
free(body);
if (!root) continue;
const char *osn = pmm_os_name(pmm_detect_os());
const char *arn = pmm_detect_arch();
const char *best = NULL;
JsonValue *vr = json_get(root, "variants");
if (vr && vr->count > 0) {
for (int i = 0; i < vr->count; i++) {
JsonValue *v = json_at(vr, i);
if (!v || v->type != JSON_OBJECT) continue;
const char *vos = json_str(v, "os");
if (!vos || (strcmp(vos, osn) != 0 && strcmp(vos, "any") != 0)) continue;
const char *va = json_str(v, "arch");
if (va && va[0] && strcmp(va, arn) != 0 && strcmp(va, "any") != 0) continue;
const char *vv = json_str(v, "version");
if (!vv) continue;
if (!best || cmp_version(vv, best) > 0) best = vv;
}
} else {
best = json_str(root, "version");
}
/* Copy best into a stable buffer BEFORE freeing the JSON tree: best
* currently points into json_str()'s internal storage. */
char bestbuf[128];
if (best) snprintf(bestbuf, sizeof(bestbuf), "%s", best);
json_free(root);
if (!best) continue;
if (cmp_version(bestbuf, curver) <= 0) { pmm_info("%s %s is up to date (latest %s)\n", pkg, curver, bestbuf); continue; }
pmm_info("%s: %s -> %s\n", pkg, curver, bestbuf);
if (!yes) {
printf(" upgrade? [y/N] ");
fflush(stdout);
char ans[8] = "";
if (!fgets(ans, sizeof(ans), stdin)) ans[0] = '\0';
if (!(ans[0] == 'y' || ans[0] == 'Y')) continue;
}
if (install_from_registry(pkg, NULL, mirror.name) == 0) { upgraded++; }
else pmm_error("failed to upgrade %s\n", pkg);
}
closedir(d);
free(cfg.registry_url); free(cfg.mirror_name);
free(mirror.name); free(mirror.api_base);
if (checked == 0) pmm_info("no installed .pdm packages.\n");
else pmm_success("upgraded %d package(s).\n", upgraded);
return 0;
}
/* Write `body` to ~/.pmm/cache/registry/<rel>. Returns 0 on success. */
static int save_registry_cache(const char *rel, const char *body) {
char cache[1024];
pmm_cache_dir(cache, sizeof(cache));
char dir[1200];
snprintf(dir, sizeof(dir), "%s/registry", cache);
/* mkdir -p via a small loop */
char tmp[1200]; snprintf(tmp, sizeof(tmp), "%s", dir);
for (char *p = tmp + 1; *p; p++) if (*p == '/') { char c = *p; *p = '\0'; PMM_MKDIR(tmp); *p = c; }
PMM_MKDIR(dir);
char path[1400];
snprintf(path, sizeof(path), "%s/%s", dir, rel);
FILE *f = fopen(path, "wb");
if (!f) return -1;
fwrite(body, 1, strlen(body), f);
fclose(f);
return 0;
}
/* pmm update — apt-style: refresh the local registry index. Downloads
* packages.json plus every {pkg}.json from the configured mirrors into
* ~/.pmm/cache/registry/ so search/info/upgrade can consult them. */
static int cmd_update(void) {
int status = 0;
char *body = registry_fetch("packages.json", &status);
if (!body) { pmm_error("no registry index (packages.json) available\n"); return 1; }
JsonValue *root = json_parse(body);
if (!root || root->type != JSON_ARRAY) {
pmm_error("bad registry index\n"); if (root) json_free(root); free(body); return 1;
}
int total = root->count, ok = 0;
if (total == 0) { pmm_info("registry index is empty.\n"); json_free(root); free(body); return 0; }
save_registry_cache("packages.json", body);
json_free(root);
for (int i = 0; i < total; i++) {
/* re-parse to get each name */
JsonValue *r2 = json_parse(body);
if (!r2) break;
JsonValue *v = json_at(r2, i);
const char *name = (v && v->type == JSON_STRING) ? v->string : NULL;
if (!name) { json_free(r2); continue; }
char rel[512]; snprintf(rel, sizeof(rel), "%s.json", name);
int st = 0;
char *pkgbody = registry_fetch(rel, &st);
if (pkgbody && st != 404 && st != 403 && st != 503) {
if (save_registry_cache(rel, pkgbody) == 0) ok++;
}
free(pkgbody);
json_free(r2);
}
free(body);
pmm_success("updated registry index: %d/%d packages cached.\n", ok, total);
return 0;
}
/* pmm verify <file> — print sha256 (and sha1) of a downloaded package file. */
static int cmd_verify(int argc, char **argv) {
if (argc < 1) { pmm_error("usage: pmm verify <file>\n"); return 1; }
const char *file = argv[0];
char hex[128];
if (pmm_sha256_file(file, hex) == 0) pmm_success("sha256 %s %s\n", hex, file);
else { pmm_error("cannot read %s\n", file); return 1; }
if (pmm_sha1_file(file, hex) == 0) pmm_success("sha1 %s %s\n", hex, file);
return 0;
}
/* Recursively delete a directory (for the download cache). */
static void rm_tree(const char *dir) {
#ifdef _WIN32
char pat[1200]; snprintf(pat, sizeof(pat), "%s\\*", dir);
WIN32_FIND_DATAA fd; HANDLE h = FindFirstFileA(pat, &fd);
if (h != INVALID_HANDLE_VALUE) {
do {
if (strcmp(fd.cFileName, ".") == 0 || strcmp(fd.cFileName, "..") == 0) continue;
char full[1400]; snprintf(full, sizeof(full), "%s\\%s", dir, fd.cFileName);
if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) rm_tree(full);
else DeleteFileA(full);
} while (FindNextFileA(h, &fd));
FindClose(h);
}
RemoveDirectoryA(dir);
#else
DIR *d = opendir(dir);
if (!d) return;
struct dirent *e;
while ((e = readdir(d))) {
if (strcmp(e->d_name, ".") == 0 || strcmp(e->d_name, "..") == 0) continue;
char full[1400]; snprintf(full, sizeof(full), "%s/%s", dir, e->d_name);
struct stat st;
if (stat(full, &st) == 0 && S_ISDIR(st.st_mode)) rm_tree(full);
else remove(full);
}
closedir(d);
rmdir(dir);
#endif
}
/* pmm cache clean — wipe the download cache. */
static int cmd_cache_clean(void) {
char dir[1024];
pmm_cache_dir(dir, sizeof(dir));
pmm_info("cleaning cache %s\n", dir);
rm_tree(dir);
return 0;
}
/* <flag> <repo-ish> [--host <name>] */
static int cmd_install_git(int argc, char **argv, const char *flag) {
const char *repo = NULL;
const char *branch = NULL;
PmmHost host = HOST_AUTO;
PmmConfig cfg; MirrorSel mirror;
load_config(&cfg);
load_mirror(&cfg, &mirror);
if (strcmp(flag, "github") == 0) host = HOST_GITHUB;
else if (strcmp(flag, "gitlab") == 0) host = HOST_GITLAB;
else if (strcmp(flag, "gitea") == 0) host = HOST_GITEA;
else if (strcmp(flag, "forgejo") == 0) host = HOST_FORGEJO;
for (int i = 0; i < argc; i++) {
if (strcmp(argv[i], "--host") == 0 && i + 1 < argc) {
host = host_from_name(argv[++i]);
if (host == HOST_UNKNOWN) {
pmm_error("unknown host '%s'\n", argv[i]);
return 1;
}
} else if ((strcmp(argv[i], "-b") == 0 || strcmp(argv[i], "--branch") == 0) && i + 1 < argc) {
branch = argv[++i];
} else if (!repo) {
repo = argv[i];
}
}
if (!repo) {
pmm_error(
"usage:\n"
" pmm install --git <repo-url.git> any git host (auto-detected API)\n"
" pmm install --github <owner/repo> GitHub\n"
" pmm install --gitlab <owner/repo> GitLab\n"
" pmm install --gitea <url/owner/repo> Gitea / Forgejo (incl. self-hosted)\n"
" [--host github|gitlab|gitea|forgejo] force host type\n");
return 1;
}
if (host == HOST_AUTO) host = repo_detect_host(repo);
RepoContext *ctx = repo_open(host, repo, mirror.api_base);
if (!ctx) return 1;
if (branch && *branch) { free(ctx->ref); ctx->ref = strdup(branch); }
pmm_info("host=%s repo=%s%s%s\n", host_name(host), repo,
mirror.name ? " mirror=" : "", mirror.name ? mirror.name : "");
PmmOS os = pmm_detect_os();
ReleaseAsset *asset = repo_latest_asset(ctx, os);
if (!asset && host == HOST_AUTO)
pmm_warn("could not reach a known release API for %s "
"(tried gitea/gitlab/github shapes)\n", repo);
if (!asset) {
if (host != HOST_AUTO)
pmm_error("no suitable %s asset found in latest release of %s\n",
pmm_os_name(os), repo);
repo_close(ctx);
return 1;
}
pmm_info("%s -> %s (tag %s)\n", pmm_os_name(os), asset->name, asset->tag);
int rc = install_file(asset->url, asset->name);
/* fallback: the first pick may have been a cross-platform/wrong asset
* (e.g. fzf's Android binary named without an OS hint). Re-fetch a clearly
* os-named one and install that instead. */
if (rc != 0 && host != HOST_AUTO) {
const char *sub = (os == OS_LINUX) ? "linux" : (os == OS_MACOS) ? "darwin" : NULL;
if (sub) {
ReleaseAsset *fb = repo_asset_matching(ctx, os, sub);
if (fb && strcmp(fb->name, asset->name) != 0) {
pmm_warn("retrying with %s (%s)\n", fb->name, pmm_os_name(os));
rc = install_file(fb->url, fb->name);
repo_asset_free(fb);
}
}
}
repo_asset_free(asset);
repo_close(ctx);
return rc == 0 ? 0 : 1;
}
static int cmd_mirror(int argc, char **argv) {
char dir[1024], path[1200];
pmm_config_dir(dir, sizeof(dir));
snprintf(path, sizeof(path), "%s/mirror.ini", dir);
if (argc == 0 || strcmp(argv[0], "list") == 0) {
Ini *ini = ini_load(path);
if (!ini || !ini->head) { printf("(no mirrors configured)\n"); ini_free(ini); return 0; }
printf("mirrors in %s:\n", path);
/* one line per unique section */
char lastsec[512] = "";
for (IniEntry *e = ini->head; e; e = e->next) {
if (!*e->section) continue;
if (strcmp(e->section, lastsec) == 0) continue;
strncpy(lastsec, e->section, sizeof(lastsec) - 1);
const char *api = ini_get(ini, e->section, "api");
const char *reg = ini_get(ini, e->section, "registry");
const char *dl = ini_get(ini, e->section, "download");
const char *dflt = ini_get(ini, e->section, "default");
printf(" [%s] registry=%s api=%s download=%s%s\n", e->section,
reg ? reg : "-", api ? api : "-", dl ? dl : "-",
dflt && strcmp(dflt, "true") == 0 ? " (default)" : "");
}
ini_free(ini);
return 0;
}
if (strcmp(argv[0], "add") == 0 && argc >= 3) {
FILE *f = fopen(path, "a");
if (!f) { pmm_error("cannot write %s\n", path); return 1; }
fprintf(f, "\n[%s]\napi = %s\n", argv[1], argv[2]);
fclose(f);
pmm_success("mirror '%s' added\n", argv[1]);
return 0;
}
if (strcmp(argv[0], "use") == 0 && argc >= 2) {
/* set mirror=<name> in pmm.conf (always writable text format) */
char cfgpath[1200];
snprintf(cfgpath, sizeof(cfgpath), "%s/pmm.conf", dir);
FILE *rf = fopen(cfgpath, "r");
char lines[256][1024]; int n = 0;
if (rf) {
char line[1024];
while (fgets(line, sizeof(line), rf) && n < 256) {
if (strncmp(line, "mirror", 6) != 0) strncpy(lines[n++], line, sizeof(lines[0]) - 1);
}
fclose(rf);
}
FILE *wf = fopen(cfgpath, "w");
if (!wf) { pmm_error("cannot write %s\n", cfgpath); return 1; }
int wrote = 0;
for (int i = 0; i < n; i++) fputs(lines[i], wf);
fprintf(wf, "mirror = %s\n", argv[1]);
(void)wrote;
fclose(wf);
pmm_success("active mirror set to '%s'\n", argv[1]);
return 0;
}
if (strcmp(argv[0], "remove") == 0 && argc >= 2) {
Ini *ini = ini_load(path);
if (!ini) { pmm_error("no mirror file\n"); return 1; }
FILE *f = fopen(path, "w");
if (!f) { pmm_error("cannot write %s\n", path); ini_free(ini); return 1; }
char cursec[512] = "";
for (IniEntry *e = ini->head; e; e = e->next) {
if (strcmp(e->section, cursec) != 0) {
strncpy(cursec, e->section, sizeof(cursec) - 1);
if (strcmp(e->section, argv[1]) != 0)
fprintf(f, "\n[%s]\n", e->section);
}
if (strcmp(e->section, argv[1]) != 0)
fprintf(f, "%s = %s\n", e->key, e->value);
}
fclose(f);
ini_free(ini);
pmm_success("mirror '%s' removed\n", argv[1]);
return 0;
}
pmm_error("usage: pmm mirror list|add|use|remove ...\n");
return 1;
}
static void print_help(void) {
printf("ParlzPackageManger (PMM) v%s\n\n", PMM_VERSION);
printf("usage:\n");
printf(" pmm install <pkg> install from registry mirrors (apt-style)\n");
printf(" pmm install <file.pdm> install a local .pdm package\n");
printf(" pmm install <pkg> [pkg2 ...] install several packages\n");
printf(" pmm install <file.deb|file.msi|file.rpm|...> install a local package file\n");
printf(" pmm install -dpkg <x.deb> install a .deb (Linux)\n");
printf(" pmm install -msi <x.msi> install an .msi (Windows)\n");
printf(" pmm install -rpm <x.rpm> install an .rpm (any Linux, incl. Debian/RPM)\n");
printf(" pmm install <https://...> install a file from a URL\n");
printf(" pmm search <keyword> list registry packages matching keyword\n");
printf(" pmm info <package|file.pdm> registry/local package info\n");
printf(" pmm verify <file> print sha256/sha1 of a package file\n");
printf(" pmm pack <dir> [out.pdm] pack a folder into a .pdm\n");
printf(" pmm list list installed packages\n");
printf(" pmm remove <pkg> uninstall a package\n");
printf(" pmm self-update update the pmm tool itself\n");
printf(" pmm update refresh the registry index (apt-style)\n");
printf(" pmm upgrade [--yes] upgrade all installed packages\n");
printf(" pmm install --git <repo-url.git> any git host, API auto-detected\n");
printf(" pmm install --github <owner/repo> GitHub latest release\n");
printf(" pmm install --gitlab <owner/repo> GitLab latest release\n");
printf(" pmm install --gitea <url/owner/repo> Gitea/Forgejo (incl. self-hosted)\n");
printf(" [--host github|gitlab|gitea|forgejo]\n");
printf(" pmm mirror list|add <n> <api>|use <n>|remove <n>\n");
printf(" pmm list list installed files\n");
printf(" pmm version | help\n\n");
printf("options:\n");
printf(" -p<drive> install under <DRIVE>:\\\\.pmm (e.g. -pd -> D:\\\\.pmm, -pc -> C:\\\\.pmm)\n");
printf(" 第一个 -p 会被记住,后续命令无需再写;用 -pc 切回 C 盘\n");
printf(" --no-color omit ANSI colours even on a terminal (PMM_NO_COLOR=1 too)\n");
printf(" -q, --quiet only print errors/warnings (suppress info/success)\n");
printf(" --verbose print extra detail\n\n");
printf("config: <base>/pmm.json | pmm.ini | pmm.conf (base = <drive>:\\\\.pmm 或 ~/.pmm)\n");
printf("mirrors: <base>/mirror.ini | mirror.conf\n");
printf("asset mapping: windows=exe/msi/zip/7z linux=deb/rpm/appimage/tar.* macos=dmg/pkg\n");
}
/* Parse "-p<drive>" / "-p <drive>" flags (e.g. -pd -> D:\.pmm) anywhere in
* argv, record the drive, and compact the argument list so subcommands don't
* see the flag. Returns new argc. */
static int consume_drive_flag(int argc, char **argv) {
int w = 1; /* argv[0] is the program name */
for (int r = 1; r < argc; r++) {
const char *a = argv[r];
if (a[0] == '-' && a[1] == 'p' && (a[2] >= 'A' && a[2] <= 'Z' || a[2] >= 'a' && a[2] <= 'z')) {
char dr[2] = { a[2], '\0' };
pmm_set_install_drive(dr);
continue;
}
if (strcmp(a, "-p") == 0 && r + 1 < argc) {
const char *nxt = argv[r + 1];
if (nxt[0] && (nxt[0] >= 'A' && nxt[0] <= 'Z' || nxt[0] >= 'a' && nxt[0] <= 'z') && nxt[1] == '\0') {
char dr[2] = { nxt[0], '\0' };
pmm_set_install_drive(dr);
r++; /* consume the drive arg too */
continue;
}
/* -p <exact path>: install under this full address */
pmm_set_install_path(nxt);
r++;
continue;
}
argv[w++] = argv[r];
}
return w;
}
/* Strip global output flags (--no-color/-q/--quiet/--verbose) from argv before
* dispatch, so every subcommand gets the same behaviour without each having to
* parse them. Also honours PMM_NO_COLOR=1 env. Returns the new argc. */
static int consume_global_flags(int argc, char **argv) {
int w = 1;
for (int r = 1; r < argc; r++) {
const char *a = argv[r];
if (strcmp(a, "--no-color") == 0) { pmm_no_color = 1; continue; }
if (strcmp(a, "-q") == 0 || strcmp(a, "--quiet") == 0) { pmm_log_level = 1; continue; }
if (strcmp(a, "--verbose") == 0) { pmm_log_level = 2; continue; }
argv[w++] = argv[r];
}
if (getenv("PMM_NO_COLOR")) pmm_no_color = 1;
return w;
}
int main(int argc, char **argv) {
#ifdef _WIN32
SetConsoleOutputCP(CP_UTF8); /* UTF-8 output so Chinese help/version isn't mojibake */
setvbuf(stdout, NULL, _IONBF, 0);
/* Enable ANSI/VT colour on modern Windows consoles (legacy gets it too via
* ENABLE_VIRTUAL_TERMINAL_PROCESSING). Without this, "\033[31m" shows as
* literal text. Do it for both stdout and stderr. */
{
HANDLE h = GetStdHandle(STD_OUTPUT_HANDLE);
if (h && h != INVALID_HANDLE_VALUE) {
DWORD m = 0;
if (GetConsoleMode(h, &m)) SetConsoleMode(h, m | ENABLE_VIRTUAL_TERMINAL_PROCESSING);
}
h = GetStdHandle(STD_ERROR_HANDLE);
if (h && h != INVALID_HANDLE_VALUE) {
DWORD m = 0;
if (GetConsoleMode(h, &m)) SetConsoleMode(h, m | ENABLE_VIRTUAL_TERMINAL_PROCESSING);
}
}
#endif
if (argc < 2) { print_help(); return 0; }
pmm_set_self_path(argv[0]);
argc = consume_drive_flag(argc, argv);
argc = consume_global_flags(argc, argv);
if (argc < 2) { print_help(); return 0; }
if (strcmp(argv[1], "help") == 0 || strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-h") == 0) {
print_help(); return 0;
}
if (strcmp(argv[1], "version") == 0 || strcmp(argv[1], "--version") == 0 || strcmp(argv[1], "-v") == 0) {
pmm_info("pmm %s (%s) [%s]\n", PMM_VERSION, pmm_os_name(pmm_detect_os()), pmm_detect_arch());
return 0;
}
if (strcmp(argv[1], "list") == 0) { pdm_list_installed(); return 0; }
if (strcmp(argv[1], "mirror") == 0) return cmd_mirror(argc - 2, argv + 2);
/* pmm remove <pkg> (also used by the Windows "Uninstall" registration) */
if (strcmp(argv[1], "remove") == 0) {
if (argc < 3) { pmm_error("usage: pmm remove <pkg>\n"); return 1; }
return pdm_remove(argv[2]) == 0 ? 0 : 1;
}
/* pmm self-update: install the latest 'pmm' tool package (auto os+arch) */
if (strcmp(argv[1], "self-update") == 0) {
PmmConfig cfg; MirrorSel mirror;
load_config(&cfg);
load_mirror(&cfg, &mirror);
pmm_info("self-update -> installing latest pmm (%s/%s)\n",
pmm_os_name(pmm_detect_os()), pmm_detect_arch());
int rc = install_from_registry("pmm", NULL, mirror.name);
free(cfg.registry_url); free(cfg.mirror_name);
free(mirror.name); free(mirror.api_base);
if (rc == 0)
pmm_success("updated. New tools are under %s/root/bin (pmm/pdm). Re-run from there, or add it to PATH.\n",
pmm_config_dir((char[1024]){0}, 1024));
return rc == 0 ? 0 : 1;
}
if (strcmp(argv[1], "update") == 0)
return cmd_update();
if (strcmp(argv[1], "upgrade") == 0)
return cmd_upgrade(argc - 2, argv + 2);
if (strcmp(argv[1], "install") == 0) {
if (argc >= 3 && (strcmp(argv[2], "--git") == 0 || strcmp(argv[2], "--github") == 0 ||
strcmp(argv[2], "--gitlab") == 0 || strcmp(argv[2], "--gitea") == 0 ||
strcmp(argv[2], "--forgejo") == 0))
return cmd_install_git(argc - 3, argv + 3, argv[2] + 2);
if (argc < 3) {
pmm_error("usage: pmm install <pkg|file.deb|file.msi|file.rpm|file.pdm> [pkg2 ...]\n"
" | pmm install -dpkg <x.deb> install a .deb (Linux)\n"
" | pmm install -rpm <x.rpm> install an .rpm (any Linux, incl. Debian/RPM)\n"
" | pmm install --git <repo>\n");
return 1;
}
/* collect one or more package arguments (each may carry an inline version
* spec like nodejs>=24,<25); optional -v/--version applies to a single one */
const char *version = NULL;
int forced = 0; /* 1 = -dpkg, 2 = -msi, 3 = -rpm (install a local package by type) */
const char *items[64]; int ni = 0;
for (int i = 2; i < argc; i++) {
const char *a = argv[i];
if (strcmp(a, "--no-cache") == 0) { pmm_no_cache = 1; continue; }
if (strcmp(a, "-dpkg") == 0) { forced = 1; if (i + 1 < argc) items[ni++] = argv[++i]; continue; }
if (strcmp(a, "-msi") == 0) { forced = 2; if (i + 1 < argc) items[ni++] = argv[++i]; continue; }
if (strcmp(a, "-rpm") == 0) { forced = 3; if (i + 1 < argc) items[ni++] = argv[++i]; continue; }
if (strncmp(a, "-v", 2) == 0 && a[2]) { version = a + 2; continue; }
if (strcmp(a, "-v") == 0 && i + 1 < argc) { version = argv[++i]; continue; }
if (strcmp(a, "--version") == 0 && i + 1 < argc) { version = argv[++i]; continue; }
if (strncmp(a, "--version=", 10) == 0) { version = a + 10; continue; }
if (a[0] != '-' && ni < 64) items[ni++] = a;
}
if (ni == 0) items[ni++] = argv[2];
PmmConfig cfg; MirrorSel mirror;
load_config(&cfg);
load_mirror(&cfg, &mirror);
int ok = 0;
for (int k = 0; k < ni; k++) {
const char *raw = items[k];
char pkg[256], spec[256];
if (ni == 1 && version && *version) {
strncpy(pkg, raw, sizeof(pkg) - 1); pkg[sizeof(pkg)-1] = '\0';
strncpy(spec, version, sizeof(spec) - 1); spec[sizeof(spec)-1] = '\0';
} else {
const char *ops[] = { ">=", "<=", "==", "!=", ">", "<", NULL };
const char *pos = NULL;
for (int i = 0; ops[i]; i++) { pos = strstr(raw, ops[i]); if (pos) break; }
if (pos) {
size_t n = (size_t)(pos - raw);
if (n >= sizeof(pkg)) n = sizeof(pkg) - 1;
memcpy(pkg, raw, n); pkg[n] = '\0';
strncpy(spec, pos, sizeof(spec) - 1); spec[sizeof(spec)-1] = '\0';
} else {
strncpy(pkg, raw, sizeof(pkg) - 1); pkg[sizeof(pkg)-1] = '\0';
spec[0] = '\0';
}
}
size_t la = strlen(pkg);
if (la > 4 && (strcmp(pkg + la - 4, ".pdm") == 0 || strcmp(pkg + la - 4, ".PDM") == 0)) {
if (pdm_install_file(pkg) == 0) ok++; else pmm_error("failed to install %s\n", pkg);
continue;
}
/* direct URL install: pmm install https://.../foo.zip
* (with -dpkg/-msi the type is forced regardless of URL extension) */
if (strncmp(pkg, "http://", 7) == 0 || strncmp(pkg, "https://", 8) == 0) {
const char *bn = strrchr(pkg, '/');
bn = bn ? bn + 1 : pkg;
if (forced == 1) bn = "download.deb";
else if (forced == 2) bn = "download.msi";
else if (forced == 3) bn = "download.rpm";
if (install_file(pkg, bn) == 0) ok++;
else pmm_error("failed to install %s\n", pkg);
continue;
}
/* local file install: forced -dpkg/-msi, or an existing installer file
* (e.g. `pmm install foo.deb` / `pmm install foo.msi`) */
if (forced == 1 || forced == 2 || forced == 3 || has_installer_ext(pkg)) {
if (install_local_file(pkg) == 0) ok++;
else pmm_error("failed to install %s\n", pkg);
continue;
}
if (install_from_registry(pkg, spec[0] ? spec : NULL, mirror.name) == 0) ok++;
}
free(cfg.registry_url); free(cfg.mirror_name);
free(mirror.name); free(mirror.api_base);
return ok == ni ? 0 : 1;
}
if (strcmp(argv[1], "pack") == 0) {
if (argc < 3) {
pmm_error("usage: pmm pack <dir> [output.pdm]\n");
return 1;
}
return pdm_pack(argv[2], argc >= 4 ? argv[3] : NULL) == 0 ? 0 : 1;
}
if (strcmp(argv[1], "search") == 0)
return cmd_search(argc - 2, argv + 2);
if (strcmp(argv[1], "cache") == 0) {
if (argc >= 3 && strcmp(argv[2], "clean") == 0) return cmd_cache_clean();
pmm_error("usage: pmm cache clean\n"); return 1;
}
if (strcmp(argv[1], "info") == 0)
return cmd_info(argc - 2, argv + 2);
if (strcmp(argv[1], "verify") == 0)
return cmd_verify(argc - 2, argv + 2);
pmm_error("unknown command '%s' (try 'pmm help')\n", argv[1]);
return 1;
}