镜像自地址
https://github.com/JGZYES/ParlzPackageManger.git
已同步 2026-09-11 05:42:44 +08:00
- 新增 src/pmm_err.h: 稳定错误码枚举 PMM_E_* (USAGE/NOT_FOUND/DOWNLOAD/ NETWORK/NO_MIRROR/REGISTRY/CHECKSUM/EXTRACT/CONFLICT/NO_TAR/NO_ROOT/CACHE/LANG/INTERNAL) - out.c: 新增 pmm_error_c(code,hint,fmt,...) 统一输出 [PMM]:[E<code>]: msg (hint: hint) - 所有高频报错点(main/pdm/install/json/ppdm)接入错误码+hint - 杜绝空 [PMM]:[ERROR]: (空消息兜底) 与裸 msg.xxx key (i18n 互补内置包回退)
1590 行
68 KiB
C
1590 行
68 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 "pmm_err.h"
|
||
#include "i18n.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) */
|
||
char *language; /* active locale / language pack ("" = default) */
|
||
} 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);
|
||
const char *l = json_str(root, "language");
|
||
if (l) cfg->language = xstrdup(l);
|
||
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);
|
||
const char *l = ini_get(ini, NULL, "language");
|
||
if (!l) l = ini_get(ini, "pmm", "language");
|
||
if (l) cfg->language = xstrdup(l);
|
||
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++) {
|
||
/* layout migration: an old .../mirror/packages base also falls back to
|
||
* .../mirror/dists, so a stale mirror.ini keeps working after the reform. */
|
||
char distsbase[2048];
|
||
snprintf(distsbase, sizeof(distsbase), "%s", bases[i]);
|
||
size_t dbl = strlen(distsbase);
|
||
static const char *pkgsuf = "/packages";
|
||
size_t dpl = strlen(pkgsuf);
|
||
int isold = (dbl >= dpl && strcmp(distsbase + dbl - dpl, pkgsuf) == 0);
|
||
if (isold) snprintf(distsbase + dbl - dpl, sizeof(distsbase) - (dbl - dpl), "/dists");
|
||
|
||
for (int attempt = 0; attempt < (isold ? 2 : 1); attempt++) {
|
||
const char *use = (attempt == 0) ? bases[i] : distsbase;
|
||
snprintf(url, sizeof(url), "%s/%s", use, rel);
|
||
body = http_get(url, &status);
|
||
if (body && status != 404 && status != 403 && status != 503) break;
|
||
free(body); body = NULL;
|
||
}
|
||
if (body) break;
|
||
}
|
||
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_c(PMM_E_USAGE, "先参考 'pmm help' 查看命令用法", "%s", pmm_tr("msg.err.usage")); return 1; }
|
||
int status = 0;
|
||
char *body = registry_fetch("packages.json", &status);
|
||
if (!body) { pmm_error_c(PMM_E_REGISTRY, "检查镜像地址是否可达: 'pmm mirror' 查看当前镜像", "%s", pmm_tr("msg.err.no-registry-index")); return 1; }
|
||
JsonValue *root = json_parse(body);
|
||
free(body);
|
||
if (!root || root->type != JSON_ARRAY) { pmm_error_c(PMM_E_REGISTRY, "镜像索引损坏, 尝试 'pmm fetch --fresh' 刷新", "%s", pmm_tr("msg.err.bad-registry-index")); 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("%s", pmm_tr_fmt("msg.no-match", 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_c(PMM_E_USAGE, "用法: pmm info <软件包名|文件.pdm>", "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_c(PMM_E_NOT_FOUND, "检查包名拼写, 或先用 'pmm search' 搜索", "%s", pmm_tr_fmt("msg.err.not-found", pkg)); return 1; }
|
||
JsonValue *root = json_parse(body);
|
||
free(body);
|
||
if (!root || root->type != JSON_OBJECT) { pmm_error_c(PMM_E_REGISTRY, "该包在镜像中的条目异常, 可尝试刷新镜像", "%s", pmm_tr_fmt("msg.err.bad-entry", 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("%s", pmm_tr_fmt("msg.description", 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;
|
||
}
|
||
|
||
/* List the *.info files in `dir` into a caller array `names[]` (up to `cap`
|
||
* entries). Returns the count. */
|
||
static int list_info_files(const char *dir, char names[][1400], int cap) {
|
||
int n = 0;
|
||
#ifdef _WIN32
|
||
char pat[1500]; snprintf(pat, sizeof(pat), "%s\\*.info", dir);
|
||
WIN32_FIND_DATAA fd; HANDLE h = FindFirstFileA(pat, &fd);
|
||
if (h != INVALID_HANDLE_VALUE) {
|
||
do {
|
||
if (n < cap) snprintf(names[n++], 1400, "%s/%s", dir, fd.cFileName);
|
||
} while (FindNextFileA(h, &fd));
|
||
FindClose(h);
|
||
}
|
||
#else
|
||
DIR *d = opendir(dir);
|
||
if (!d) return 0;
|
||
struct dirent *e;
|
||
while ((e = readdir(d)) != NULL) {
|
||
size_t ln = strlen(e->d_name);
|
||
if (ln < 5 || strcmp(e->d_name + ln - 5, ".info") != 0) continue;
|
||
if (n < cap) snprintf(names[n++], 1400, "%s/%s", dir, e->d_name);
|
||
}
|
||
closedir(d);
|
||
#endif
|
||
return n;
|
||
}
|
||
|
||
/* 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++) {
|
||
const char *a = argv[i];
|
||
if (strcmp(a, "--yes") == 0) { yes = 1; continue; }
|
||
/* a short flag cluster (-y, -qy, -yq): any 'y' means yes */
|
||
if (a[0] == '-' && a[1] && a[1] != '-') {
|
||
for (const char *p = a + 1; *p; p++) if (*p == 'y') { yes = 1; break; }
|
||
}
|
||
}
|
||
|
||
char home[1024];
|
||
pmm_config_dir(home, sizeof(home));
|
||
char db[1200];
|
||
snprintf(db, sizeof(db), "%s/installed", home);
|
||
|
||
PmmConfig cfg; MirrorSel mirror;
|
||
load_config(&cfg); load_mirror(&cfg, &mirror);
|
||
|
||
char files[128][1400];
|
||
int nfiles = list_info_files(db, files, 128);
|
||
if (nfiles == 0) { pmm_info("%s", pmm_tr("msg.no-installed")); return 0; }
|
||
|
||
int upgraded = 0, checked = 0;
|
||
for (int fi = 0; fi < nfiles; fi++) {
|
||
const char *ipath = files[fi];
|
||
char info[4096];
|
||
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("%s", pmm_tr_fmt("msg.skipping-no-registry", 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", pmm_tr_fmt("msg.up-to-date", 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_c(PMM_E_INTERNAL, "安装失败, 可先 'pmm fetch <包名>' 手动下载排查", "%s", pmm_tr_fmt("msg.err.failed-install", pkg));
|
||
}
|
||
|
||
free(cfg.registry_url); free(cfg.mirror_name);
|
||
free(mirror.name); free(mirror.api_base);
|
||
if (checked == 0) pmm_info("%s", pmm_tr("msg.no-installed"));
|
||
else pmm_success("%s", pmm_tr_fmt("msg.upgraded", upgraded));
|
||
return 0;
|
||
}
|
||
|
||
/* pmm list --upgradable — show installed packages that have a newer registry
|
||
* version, without installing anything. */
|
||
static int cmd_list_upgradable(int argc, char **argv) {
|
||
(void)argc; (void)argv;
|
||
char home[1024];
|
||
pmm_config_dir(home, sizeof(home));
|
||
char db[1200];
|
||
snprintf(db, sizeof(db), "%s/installed", home);
|
||
|
||
char files[128][1400];
|
||
int nfiles = list_info_files(db, files, 128);
|
||
if (nfiles == 0) { pmm_info("%s", pmm_tr("msg.no-installed")); return 0; }
|
||
|
||
int shown = 0;
|
||
for (int fi = 0; fi < nfiles; fi++) {
|
||
char info[4096];
|
||
FILE *fp = fopen(files[fi], "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;
|
||
|
||
char rel[512]; snprintf(rel, sizeof(rel), "%s.json", pkg);
|
||
int status = 0;
|
||
char *body = registry_fetch(rel, &status);
|
||
if (!body) 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");
|
||
}
|
||
char bestbuf[128];
|
||
if (best) snprintf(bestbuf, sizeof(bestbuf), "%s", best);
|
||
json_free(root);
|
||
if (!best) continue;
|
||
|
||
if (cmp_version(bestbuf, curver) > 0) {
|
||
printf(" %-20s %s -> %s\n", pkg, curver, bestbuf);
|
||
shown++;
|
||
}
|
||
}
|
||
if (shown == 0) pmm_info("%s\n", "no upgradable packages.");
|
||
else pmm_info("%d upgradable package(s).\n", shown);
|
||
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_c(PMM_E_REGISTRY, "检查镜像地址: 'pmm mirror' 查看当前镜像", "no registry index (packages.json) available\n"); return 1; }
|
||
JsonValue *root = json_parse(body);
|
||
if (!root || root->type != JSON_ARRAY) {
|
||
pmm_error_c(PMM_E_REGISTRY, "镜像索引损坏, 尝试 'pmm fetch --fresh' 刷新", "bad registry index\n"); if (root) json_free(root); free(body); return 1;
|
||
}
|
||
int total = root->count, ok = 0;
|
||
if (total == 0) { pmm_info("%s", pmm_tr("msg.registry-empty")); 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("%s", pmm_tr_fmt("msg.registry-updated", 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_c(PMM_E_USAGE, "用法: pmm verify <文件>", "usage: pmm verify <file>\n"); return 1; }
|
||
const char *file = argv[0];
|
||
char hex[128];
|
||
if (pmm_sha256_file(file, hex) == 0) pmm_success("%s", pmm_tr_fmt("msg.sha256", hex, file));
|
||
else { pmm_error_c(PMM_E_NOT_FOUND, "检查文件路径是否存在且可读", "%s", pmm_tr_fmt("msg.err.cannot-read", file)); return 1; }
|
||
if (pmm_sha1_file(file, hex) == 0) pmm_success("%s", pmm_tr_fmt("msg.sha1", 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("%s", pmm_tr_fmt("msg.cleaning-cache", 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_c(PMM_E_USAGE, "可用主机: github / gitlab / gitea / forgejo", "%s", pmm_tr_fmt("msg.err.unknown-host", 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_c(PMM_E_USAGE,
|
||
"用法: pmm install --git <repo-url.git> / --github <owner/repo> / --gitlab <owner/repo> / --gitea <url/owner/repo>",
|
||
"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_c(PMM_E_NOT_FOUND, "该仓库没有适配当前系统的发布文件, 换用 --git 完整 URL", "%s", pmm_tr_fmt("msg.err.no-suitable-asset", 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("%s", pmm_tr_fmt("msg.retrying-asset", 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;
|
||
}
|
||
/* pmm mirror check — probe each configured registry mirror's reachability
|
||
* (apt-style source check). Reports priority + ok/fail for each source. */
|
||
if (strcmp(argv[0], "check") == 0) {
|
||
Ini *ini = ini_load(path);
|
||
if (!ini || !ini->head) { pmm_error_c(PMM_E_NO_MIRROR, "先添加镜像: 'pmm mirror add <名称> <api地址>'", "%s", pmm_tr("msg.err.no-mirror")); ini_free(ini); return 1; }
|
||
printf("checking registry mirrors...\n");
|
||
char lastsec[512] = "";
|
||
int reachable = 0, total = 0;
|
||
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 *reg = ini_get(ini, e->section, "registry");
|
||
const char *pri = ini_get(ini, e->section, "priority");
|
||
const char *dflt = ini_get(ini, e->section, "default");
|
||
if (!reg || !*reg) continue; /* source without a registry isn't probeable */
|
||
total++;
|
||
char url[2048];
|
||
snprintf(url, sizeof(url), "%s/packages.json", reg);
|
||
/* probe with a short GET; capture status code */
|
||
int st = 0;
|
||
char *body = http_get(url, &st);
|
||
int ok = (body && st != 404 && st != 403 && st != 503 && st != 0);
|
||
free(body);
|
||
const char *stext = ok ? "ok" :
|
||
(st == 404) ? "not found" :
|
||
(st == 0) ? "unreachable" : "HTTP %d";
|
||
printf(" [%s] pri=%s %s%s %s\n", e->section,
|
||
pri ? pri : "-", reg, dflt && strcmp(dflt,"true")==0 ? " (default)" : "",
|
||
ok ? "OK" : stext);
|
||
if (ok) reachable++;
|
||
}
|
||
ini_free(ini);
|
||
printf("result: %d/%d sources reachable\n", reachable, total);
|
||
return reachable > 0 ? 0 : 1;
|
||
}
|
||
if (strcmp(argv[0], "add") == 0 && argc >= 3) {
|
||
FILE *f = fopen(path, "a");
|
||
if (!f) { pmm_error_c(PMM_E_INTERNAL, "检查配置文件目录是否可写(系统模式需 sudo)", "%s", pmm_tr_fmt("msg.err.cannot-write", path)); return 1; }
|
||
fprintf(f, "\n[%s]\napi = %s\n", argv[1], argv[2]);
|
||
fclose(f);
|
||
pmm_success("%s", pmm_tr_fmt("msg.mirror-added", 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_c(PMM_E_INTERNAL, "检查配置文件目录是否可写(系统模式需 sudo)", "%s", pmm_tr_fmt("msg.err.cannot-write", 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("%s", pmm_tr_fmt("msg.mirror-active", argv[1]));
|
||
return 0;
|
||
}
|
||
if (strcmp(argv[0], "remove") == 0 && argc >= 2) {
|
||
Ini *ini = ini_load(path);
|
||
if (!ini) { pmm_error_c(PMM_E_NO_MIRROR, "检查镜像配置文件是否存在", "%s", pmm_tr("msg.err.no-mirror-file")); return 1; }
|
||
FILE *f = fopen(path, "w");
|
||
if (!f) { pmm_error_c(PMM_E_INTERNAL, "检查配置文件目录是否可写(系统模式需 sudo)", "%s", pmm_tr_fmt("msg.err.cannot-write", 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("%s", pmm_tr_fmt("msg.mirror-removed", argv[1]));
|
||
return 0;
|
||
}
|
||
pmm_error_c(PMM_E_USAGE, "用法: pmm mirror <add|use|remove|check|list> ...", "%s", pmm_tr("msg.err.usage"));
|
||
return 1;
|
||
}
|
||
|
||
static unsigned long long g_clean_size;
|
||
static int g_clean_count;
|
||
|
||
/* Recursively delete every entry under `path`, keeping `path` itself and
|
||
* accumulating the number/size of files removed. Used by `pmm clean`. */
|
||
static void clean_rm(const char *path) {
|
||
#ifdef _WIN32
|
||
char patt[1200]; snprintf(patt, sizeof(patt), "%s\\*", path);
|
||
WIN32_FIND_DATAA fd; HANDLE h = FindFirstFileA(patt, &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", path, fd.cFileName);
|
||
if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
|
||
clean_rm(full);
|
||
RemoveDirectoryA(full);
|
||
} else {
|
||
ULONGLONG sz = ((ULONGLONG)fd.nFileSizeHigh << 32) | fd.nFileSizeLow;
|
||
g_clean_size += (unsigned long long)sz; g_clean_count++;
|
||
DeleteFileA(full);
|
||
}
|
||
} while (FindNextFileA(h, &fd));
|
||
FindClose(h);
|
||
}
|
||
#else
|
||
DIR *d = opendir(path);
|
||
if (d) {
|
||
struct dirent *e;
|
||
while ((e = readdir(d)) != NULL) {
|
||
if (strcmp(e->d_name, ".") == 0 || strcmp(e->d_name, "..") == 0) continue;
|
||
char full[1400]; snprintf(full, sizeof(full), "%s/%s", path, e->d_name);
|
||
struct stat st;
|
||
if (stat(full, &st) == 0) {
|
||
if (S_ISDIR(st.st_mode)) { clean_rm(full); rmdir(full); }
|
||
else { g_clean_size += (unsigned long long)st.st_size; g_clean_count++; remove(full); }
|
||
}
|
||
}
|
||
closedir(d);
|
||
}
|
||
#endif
|
||
}
|
||
|
||
/* pmm clean — drop everything under the cache dir (~/.pmm/cache) but keep it. */
|
||
static int cmd_clean(void) {
|
||
char cache[1024];
|
||
pmm_cache_dir(cache, sizeof(cache));
|
||
g_clean_size = 0; g_clean_count = 0;
|
||
clean_rm(cache);
|
||
pmm_success("%s", pmm_tr_fmt("msg.cache-cleared", g_clean_count, (double)g_clean_size / 1024.0));
|
||
return 0;
|
||
}
|
||
|
||
/* ---- pmm cache [list|clean] ---- */
|
||
static unsigned long long g_list_size;
|
||
static int g_list_count;
|
||
static void cache_list_walk(const char *path, int top) {
|
||
#ifdef _WIN32
|
||
char patt[1200]; snprintf(patt, sizeof(patt), "%s\\*", path);
|
||
WIN32_FIND_DATAA fd; HANDLE h = FindFirstFileA(patt, &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", path, fd.cFileName);
|
||
if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) cache_list_walk(full, 0);
|
||
else { ULONGLONG sz = ((ULONGLONG)fd.nFileSizeHigh << 32) | fd.nFileSizeLow;
|
||
printf(" %s\n", full); g_list_count++; g_list_size += (unsigned long long)sz; }
|
||
} while (FindNextFileA(h, &fd));
|
||
FindClose(h);
|
||
}
|
||
#else
|
||
DIR *d = opendir(path);
|
||
if (d) {
|
||
struct dirent *e;
|
||
while ((e = readdir(d)) != NULL) {
|
||
if (strcmp(e->d_name, ".") == 0 || strcmp(e->d_name, "..") == 0) continue;
|
||
char full[1400]; snprintf(full, sizeof(full), "%s/%s", path, e->d_name);
|
||
struct stat st;
|
||
if (stat(full, &st) == 0) {
|
||
if (S_ISDIR(st.st_mode)) cache_list_walk(full, 0);
|
||
else { printf(" %s\n", full); g_list_count++; g_list_size += (unsigned long long)st.st_size; }
|
||
}
|
||
}
|
||
closedir(d);
|
||
}
|
||
#endif
|
||
(void)top;
|
||
}
|
||
static int cmd_cache(int argc, char **argv) {
|
||
if (argc > 0 && strcmp(argv[0], "clean") == 0) return cmd_clean();
|
||
char cache[1024];
|
||
pmm_cache_dir(cache, sizeof(cache));
|
||
g_list_size = 0; g_list_count = 0;
|
||
printf("cache: %s\n", cache);
|
||
cache_list_walk(cache, 1);
|
||
printf("(%d 个文件, %.1f KB)\n", g_list_count, (double)g_list_size / 1024.0);
|
||
return 0;
|
||
}
|
||
|
||
/* ---- pmm fetch <pkg>... : predownload packages into the cache (no install) ---- */
|
||
static int cmd_fetch(int argc, char **argv) {
|
||
if (argc < 1) { pmm_error_c(PMM_E_USAGE, "用法: pmm fetch <软件包名>...", "用法: pmm fetch <pkg>...\n"); return 1; }
|
||
pmm_fetch_only = 1;
|
||
int ok = 0;
|
||
for (int i = 0; i < argc; i++)
|
||
if (install_from_registry(argv[i], NULL, NULL) == 0) ok++;
|
||
pmm_fetch_only = 0;
|
||
pmm_success("已抓取 %d/%d 个包到缓存\n", ok, argc);
|
||
return ok == argc ? 0 : 1;
|
||
}
|
||
|
||
/* Return the registry "latest" version of `pkg`. We use the top-level
|
||
* `version` field of `<pkg>.json` (the mirror bumps it on every release) rather
|
||
* than walking the variants array. Returns a malloc'd string, or NULL. */
|
||
static char *registry_latest_version(const char *pkg) {
|
||
char rel[512]; snprintf(rel, sizeof(rel), "%s.json", pkg);
|
||
int status = 0;
|
||
char *body = registry_fetch(rel, &status);
|
||
if (!body) return NULL;
|
||
JsonValue *root = json_parse(body);
|
||
free(body);
|
||
if (!root) return NULL;
|
||
const char *best = json_str(root, "version");
|
||
char *out = best ? strdup(best) : NULL;
|
||
json_free(root);
|
||
return out;
|
||
}
|
||
|
||
/* pmm doctor / diagnose — one-shot health check that prints actionable fixes
|
||
* for the common "install didn't take effect" issues: which binary resolves,
|
||
* PATH ordering / Windows leak, a flaky mirror, installed-package integrity and
|
||
* a stale language pack. Read-only: it never changes anything. */
|
||
static int cmd_doctor(void) {
|
||
char home[1024];
|
||
pmm_config_dir(home, sizeof(home));
|
||
int issues = 0;
|
||
|
||
/* ---- 1. which binary does `pmm` resolve to & is it the running one ---- */
|
||
pmm_info("— 二进制解析 —\n");
|
||
const char *self = pmm_self_path();
|
||
printf(" 当前运行: %s\n", self && *self ? self : "(unknown)");
|
||
char resolved[1400] = "";
|
||
#ifndef _WIN32
|
||
FILE *f = popen("command -v pmm 2>/dev/null", "r");
|
||
if (f) { if (fgets(resolved, sizeof(resolved), f)) { size_t l = strlen(resolved);
|
||
while (l && (resolved[l-1]=='\n' || resolved[l-1]=='\r')) resolved[--l]=0; } pclose(f); }
|
||
#else
|
||
FILE *f = popen("where pmm 2>nul", "r");
|
||
if (f) { if (fgets(resolved, sizeof(resolved), f)) { size_t l = strlen(resolved);
|
||
while (l && (resolved[l-1]=='\n' || resolved[l-1]=='\r')) resolved[--l]=0; } pclose(f); }
|
||
#endif
|
||
if (resolved[0]) {
|
||
printf(" PATH 解析: %s\n", resolved);
|
||
if (self && *self && strcmp(resolved, self) == 0) {
|
||
printf(" [OK] 命中的就是当前运行的 pmm\n");
|
||
} else {
|
||
pmm_warn(" [X] PATH 里另有一个 pmm (%s),而当前运行的是 %s\n", resolved, self ? self : "?");
|
||
printf(" 修复: hash -r;若仍不对,把 %s 所在目录放到 PATH 前面(export PATH=\"<dir>:$PATH\")\n",
|
||
self ? self : "");
|
||
issues++;
|
||
}
|
||
} else {
|
||
pmm_warn(" [X] PATH 中找不到 pmm(请把安装目录加入 PATH)\n");
|
||
issues++;
|
||
}
|
||
|
||
/* ---- 2. Windows /mnt PATH leak ---- */
|
||
pmm_info("— PATH 顺序 / Windows 泄漏 —\n");
|
||
{
|
||
const char *path = getenv("PATH");
|
||
char tmp[16384];
|
||
snprintf(tmp, sizeof(tmp), "%s", path ? path : "");
|
||
int leak = 0;
|
||
char *save = NULL;
|
||
for (char *t = strtok(tmp, ":"); t; t = strtok(NULL, ":")) {
|
||
if (strncmp(t, "/mnt/", 5) == 0 && strstr(t, "pmm")) {
|
||
pmm_warn(" [X] PATH 含 Windows 泄漏目录: %s\n", t);
|
||
printf(" 说明: Windows 侧装的 pmm 被 WSL 继承。修复: 在 Windows 移除该 PATH 项,"
|
||
"或将 ~/.local/bin 放到其前面\n");
|
||
leak = 1; issues++;
|
||
}
|
||
}
|
||
if (!leak) printf(" [OK] PATH 未发现 /mnt 下的 pmm 泄漏\n");
|
||
}
|
||
|
||
/* ---- 3. mirror reachability ---- */
|
||
pmm_info("— 镜像源可达性 —\n");
|
||
{
|
||
char path2[1200];
|
||
snprintf(path2, sizeof(path2), "%s/mirror.ini", home);
|
||
Ini *ini = ini_load(path2);
|
||
if (!ini || !ini->head) {
|
||
pmm_warn(" [X] 未配置镜像 (%s)\n", path2);
|
||
printf(" 修复: pmm setting mirror add <名字> <registry 地址>,或用 install.sh 生成默认镜像\n");
|
||
issues++;
|
||
} else {
|
||
printf(" %s\n", path2);
|
||
char lastsec[512] = "";
|
||
int okcount = 0, total = 0, defbad = 0;
|
||
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 *reg = ini_get(ini, e->section, "registry");
|
||
const char *pri = ini_get(ini, e->section, "priority");
|
||
const char *def = ini_get(ini, e->section, "default");
|
||
if (!reg || !*reg) continue;
|
||
total++;
|
||
char url[2048];
|
||
snprintf(url, sizeof(url), "%s/packages.json", reg);
|
||
int st = 0;
|
||
char *body = http_get(url, &st);
|
||
int ok = (body && st != 404 && st != 403 && st != 503 && st != 0);
|
||
free(body);
|
||
int isdef = def && strcmp(def, "true") == 0;
|
||
printf(" [%s] pri=%s %s%s %s\n", e->section, pri ? pri : "-", reg,
|
||
isdef ? " (default)" : "", ok ? "OK" : st == 404 ? "404/not-found"
|
||
: st == 0 ? "unreachable" : "HTTP->" );
|
||
if (ok) okcount++; else if (isdef) defbad = 1;
|
||
}
|
||
ini_free(ini);
|
||
printf(" 结果: %d/%d 可达\n", okcount, total);
|
||
if (defbad) { pmm_warn(" [X] 默认/优先镜像不可达 — 修复: pmm setting mirror <健康镜像名>\n"); issues++; }
|
||
if (total == 0) { pmm_warn(" [X] 没有可探测的镜像\n"); issues++; }
|
||
}
|
||
}
|
||
|
||
/* ---- 4. installed package integrity ---- */
|
||
pmm_info("— 已装包完整性 —\n");
|
||
{
|
||
char db[1200], root[1200];
|
||
snprintf(db, sizeof(db), "%s/installed", home);
|
||
snprintf(root, sizeof(root), "%s/root", home);
|
||
char files[128][1400];
|
||
int n = list_info_files(db, files, 128);
|
||
if (n == 0) { printf(" (无已装包)\n"); }
|
||
else {
|
||
int bad = 0;
|
||
for (int i = 0; i < n; i++) {
|
||
char info[4096];
|
||
FILE *fp = fopen(files[i], "rb");
|
||
if (!fp) continue;
|
||
size_t g = fread(info, 1, sizeof(info)-1, fp); fclose(fp); info[g] = 0;
|
||
char pkg[256], ver[128];
|
||
installed_version(info, pkg, sizeof(pkg), ver, sizeof(ver));
|
||
if (!pkg[0]) continue;
|
||
char bin[1400];
|
||
snprintf(bin, sizeof(bin), "%s/bin/%s", root, pkg);
|
||
FILE *bf = fopen(bin, "rb");
|
||
if (!bf) {
|
||
pmm_warn(" [X] %s %s 缺可执行主文件 %s\n", pkg, ver ? ver : "", bin);
|
||
printf(" 修复: pmm remove %s && pmm install %s\n", pkg, pkg);
|
||
bad++; issues++;
|
||
} else {
|
||
fclose(bf);
|
||
printf(" [OK] %s %s\n", pkg, ver ? ver : "");
|
||
}
|
||
}
|
||
if (bad == 0) printf(" (%d 个已装包均完整)\n", n - bad);
|
||
}
|
||
}
|
||
|
||
/* ---- 5. language pack ---- */
|
||
pmm_info("— 语言包 —\n");
|
||
{
|
||
char loc[64];
|
||
snprintf(loc, sizeof(loc), "%s", pmm_lang_locale());
|
||
char lp[1400];
|
||
snprintf(lp, sizeof(lp), "%s/lang/%s.pjson", home, loc);
|
||
FILE *fp = fopen(lp, "rb");
|
||
if (!fp) { pmm_warn(" [X] 语言包 %s 缺失 — 修复: pmm setting lang %s\n", lp, loc); issues++; }
|
||
else {
|
||
char buf[65536]; size_t g = fread(buf, 1, sizeof(buf)-1, fp); fclose(fp); buf[g] = 0;
|
||
JsonValue *root = json_parse(buf);
|
||
if (!root) { pmm_warn(" [X] 语言包 %s 无法解析 — 修复: pmm setting lang %s\n", lp, loc); issues++; }
|
||
else {
|
||
static const char *probes[] = { "msg.self-up-to-date", "desc.clean", "msg.err.conflict", NULL };
|
||
int miss = 0;
|
||
for (int i = 0; probes[i]; i++) if (!json_str(root, probes[i])) miss++;
|
||
if (miss) {
|
||
pmm_warn(" [X] 语言包偏旧 (%s),缺 %d 个新 key — 修复: pmm setting lang %s\n", lp, miss, loc);
|
||
issues++;
|
||
} else printf(" [OK] %s\n", lp);
|
||
json_free(root);
|
||
}
|
||
}
|
||
}
|
||
|
||
printf("\n");
|
||
pmm_success("%s", issues == 0 ? "doctor: 一切正常 ✔" : pmm_tr_fmt("msg.doctor-issues", issues));
|
||
return issues == 0 ? 0 : 1;
|
||
}
|
||
|
||
static void print_help(void) {
|
||
printf("PMM %s (%s)\n\n", PMM_VERSION, pmm_detect_arch());
|
||
printf("%s\n", pmm_tr("help.usage"));
|
||
printf(" %-32s%s\n", "pmm install <pkg> ", pmm_tr("desc.install"));
|
||
printf(" %-32s%s\n", "pmm search <keyword> ", pmm_tr("desc.search"));
|
||
printf(" %-32s%s\n", "pmm info <pkg|file.pdm> ", pmm_tr("desc.info"));
|
||
printf(" %-32s%s\n", "pmm remove <pkg> ", pmm_tr("desc.remove"));
|
||
printf(" %-32s%s\n", "pmm update ", pmm_tr("desc.update"));
|
||
printf(" %-32s%s\n", "pmm upgrade [--yes] ", pmm_tr("desc.upgrade"));
|
||
printf(" %-32s%s\n", "pmm setting mirror ... ", pmm_tr("desc.mirror"));
|
||
printf(" %-32s%s\n", "pmm self-update ", pmm_tr("desc.self-update"));
|
||
printf(" %-32s%s\n", "pmm clean ", pmm_tr("desc.clean"));
|
||
printf(" %-32s%s\n", "pmm cache [list|clean] ", pmm_tr("desc.cache"));
|
||
printf(" %-32s%s\n", "pmm fetch <pkg> ", pmm_tr("desc.fetch"));
|
||
printf(" %-32s%s\n", "pmm doctor ", pmm_tr("desc.doctor"));
|
||
printf(" %-32s%s\n", "pmm version | help ", pmm_tr("desc.help"));
|
||
printf("\n%s\n", pmm_tr("help.options"));
|
||
printf(" %-32s%s\n", "-p<drive> ", pmm_tr("opt.p-drive"));
|
||
printf(" %-32s%s\n", "--no-color ", pmm_tr("opt.no-color"));
|
||
printf(" %-32s%s\n", "-q, --quiet", pmm_tr("opt.quiet"));
|
||
printf(" %-32s%s\n", "--verbose ", pmm_tr("opt.verbose"));
|
||
printf("\nconfig: <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");
|
||
}
|
||
|
||
/* Make every directory component of `path` using PMM_MKDIR (mkdir -p). */
|
||
static void mkdir_p_local(char *path) {
|
||
for (char *p = path + 1; *p; p++)
|
||
if (*p == '/' || *p == '\\') { char c = *p; *p = '\0'; PMM_MKDIR(path); *p = c; }
|
||
PMM_MKDIR(path);
|
||
}
|
||
|
||
/* pmm setting lang ... */
|
||
static int cmd_setting(int argc, char **argv) {
|
||
if (argc < 1) {
|
||
pmm_error_c(PMM_E_USAGE, "用法: pmm setting <get|set|mirror|lang> ...", "%s", pmm_tr("msg.err.usage"));
|
||
return 1;
|
||
}
|
||
char dir[1024];
|
||
pmm_config_dir(dir, sizeof(dir));
|
||
char cfgpath[1200];
|
||
snprintf(cfgpath, sizeof(cfgpath), "%s/pmm.conf", dir);
|
||
|
||
/* setting get — print the current pmm.conf config */
|
||
if (strcmp(argv[0], "get") == 0) {
|
||
FILE *f = fopen(cfgpath, "r");
|
||
if (!f) { pmm_info("(no config yet)\n"); return 0; }
|
||
char line[1024];
|
||
while (fgets(line, sizeof(line), f)) fputs(line, stdout);
|
||
fclose(f);
|
||
return 0;
|
||
}
|
||
|
||
/* setting set <key> <value> — persist a key=value in pmm.conf */
|
||
if (strcmp(argv[0], "set") == 0) {
|
||
if (argc < 3) { pmm_error_c(PMM_E_USAGE, "用法: pmm setting set <键> <值>", "%s", pmm_tr("msg.err.usage")); return 1; }
|
||
const char *key = argv[1], *val = argv[2];
|
||
if (!*key || strstr(key, " ") || strstr(val, "\n")) { pmm_error_c(PMM_E_USAGE, "键不能含空格, 值不能包含换行", "%s", pmm_tr("msg.err.usage")); return 1; }
|
||
char lines[256][1024]; int nn = 0;
|
||
FILE *rf = fopen(cfgpath, "r");
|
||
if (rf) {
|
||
char line[1024];
|
||
while (fgets(line, sizeof(line), rf) && nn < 256) {
|
||
if (strncmp(line, key, strlen(key)) == 0 && (line[strlen(key)] == ' ' || line[strlen(key)] == '=' || line[strlen(key)] == '\n')) continue;
|
||
strncpy(lines[nn++], line, sizeof(lines[0]) - 1);
|
||
}
|
||
fclose(rf);
|
||
}
|
||
FILE *wf = fopen(cfgpath, "w");
|
||
if (!wf) { pmm_error_c(PMM_E_INTERNAL, "检查配置文件目录是否可写(系统模式需 sudo)", "%s", pmm_tr_fmt("msg.err.cannot-write", cfgpath)); return 1; }
|
||
for (int i = 0; i < nn; i++) fputs(lines[i], wf);
|
||
fprintf(wf, "%s = %s\n", key, val);
|
||
fclose(wf);
|
||
pmm_success("%s", pmm_tr_fmt("msg.setting-set", key, val));
|
||
return 0;
|
||
}
|
||
|
||
/* setting mirror [list|check|add <name> <api>|use <name>|remove <name>|<name>]
|
||
* — all the former `pmm mirror` subcommands now live under `setting`.
|
||
* A bare name (not a known subcommand) keeps the old behaviour: set the
|
||
* active mirror (persist mirror=<name>). Bare `setting mirror` lists. */
|
||
if (strcmp(argv[0], "mirror") == 0) {
|
||
if (argc >= 2) {
|
||
const char *s = argv[1];
|
||
if (strcmp(s, "list") == 0 || strcmp(s, "check") == 0 ||
|
||
strcmp(s, "add") == 0 || strcmp(s, "use") == 0 ||
|
||
strcmp(s, "remove") == 0) {
|
||
return cmd_mirror(argc - 1, argv + 1); /* argv+1 = [sub, ...] */
|
||
}
|
||
/* mirror <name> → set active mirror */
|
||
char *fake[] = { (char*)"set", (char*)"mirror", argv[1], NULL };
|
||
return cmd_setting(3, fake);
|
||
}
|
||
return cmd_mirror(0, NULL); /* bare `setting mirror` == list */
|
||
}
|
||
|
||
if (strcmp(argv[0], "lang") != 0) {
|
||
pmm_error_c(PMM_E_USAGE, "可用子命令: get / set / mirror / lang", "%s", pmm_tr("msg.err.usage"));
|
||
return 1;
|
||
}
|
||
char langdir[1200];
|
||
snprintf(langdir, sizeof(langdir), "%s/lang", dir);
|
||
|
||
/* -l : list installed language packs */
|
||
if (argc == 2 && strcmp(argv[1], "-l") == 0) {
|
||
printf("language packs in %s:\n", langdir);
|
||
#ifdef _WIN32
|
||
char pat[1300]; snprintf(pat, sizeof(pat), "%s\\*.pjson", langdir);
|
||
WIN32_FIND_DATAA fd; HANDLE h = FindFirstFileA(pat, &fd);
|
||
if (h != INVALID_HANDLE_VALUE) {
|
||
do { printf(" %s\n", fd.cFileName); } while (FindNextFileA(h, &fd));
|
||
FindClose(h);
|
||
}
|
||
#else
|
||
DIR *dd = opendir(langdir);
|
||
if (dd) {
|
||
struct dirent *ee;
|
||
while ((ee = readdir(dd)) != NULL) {
|
||
size_t ln = strlen(ee->d_name);
|
||
if (ln > 6 && strcmp(ee->d_name + ln - 6, ".pjson") == 0) printf(" %s\n", ee->d_name);
|
||
}
|
||
closedir(dd);
|
||
}
|
||
#endif
|
||
return 0;
|
||
}
|
||
|
||
/* install / activate a specific locale: requires a value */
|
||
if (argc < 2) { pmm_error_c(PMM_E_USAGE, "用法: pmm setting lang <语言代码>", "%s", pmm_tr("msg.err.usage")); return 1; }
|
||
const char *loc = argv[1];
|
||
if (strchr(loc, '/') || strchr(loc, '\\') || strstr(loc, "..")) { pmm_error_c(PMM_E_LANG, "语言代码非法, 应为不含路径的纯名称(如 zh-CN)", "%s", pmm_tr_fmt("msg.err.invalid-locale", loc)); return 1; }
|
||
|
||
/* Try every registry mirror in priority order; a pack may only be reachable
|
||
* on a secondary mirror. Language packs live under {host}/mirror/lang/, NOT
|
||
* the registry base {host}/mirror/packages, so swap the trailing "/packages"
|
||
* (if present) for "/lang". */
|
||
MirrorList *ml = mirrors_load();
|
||
mkdir_p_local(langdir);
|
||
char out[1400];
|
||
snprintf(out, sizeof(out), "%s/%s.pjson", langdir, loc);
|
||
char url[2048], base2[2048];
|
||
int got = 0;
|
||
for (int i = 0; i < ml->count; i++) {
|
||
if (!ml->items[i].registry || !*ml->items[i].registry) continue;
|
||
snprintf(base2, sizeof(base2), "%s", ml->items[i].registry);
|
||
size_t bl = strlen(base2);
|
||
/* Language packs live at the mirror root {base}/lang/, NOT under the
|
||
* registry subpath ({base}/packages or {base}/dists). Strip whichever
|
||
* suffix is present and append /lang. */
|
||
static const char *sufs[] = { "/packages", "/dists", "/dist", NULL };
|
||
int matched = 0;
|
||
for (int k = 0; sufs[k]; k++) {
|
||
size_t sl = strlen(sufs[k]);
|
||
if (bl >= sl && strcmp(base2 + bl - sl, sufs[k]) == 0) { bl -= sl; matched = 1; break; }
|
||
}
|
||
(void)matched;
|
||
snprintf(base2 + bl, sizeof(base2) - bl, "/lang");
|
||
snprintf(url, sizeof(url), "%s/%s.pjson", base2, loc);
|
||
if (http_download(url, out) == 0) { got = 1; break; }
|
||
}
|
||
if (!got) {
|
||
pmm_error_c(PMM_E_DOWNLOAD, "检查网络连接或镜像地址, 或 'pmm mirror check'", "%s", pmm_tr_fmt("msg.err.download-failed", loc));
|
||
mirrors_free(ml); return 1;
|
||
}
|
||
mirrors_free(ml);
|
||
|
||
/* persist language=<loc> in pmm.conf (always-writable text) */
|
||
char lines[256][1024]; int nn = 0;
|
||
FILE *rf = fopen(cfgpath, "r");
|
||
if (rf) {
|
||
char line[1024];
|
||
while (fgets(line, sizeof(line), rf) && nn < 256) {
|
||
if (strncmp(line, "language", 8) != 0) strncpy(lines[nn++], line, sizeof(lines[0]) - 1);
|
||
}
|
||
fclose(rf);
|
||
}
|
||
FILE *wf = fopen(cfgpath, "w");
|
||
if (!wf) { pmm_error_c(PMM_E_INTERNAL, "检查配置文件目录是否可写(系统模式需 sudo)", "%s", pmm_tr_fmt("msg.err.cannot-write", cfgpath)); return 1; }
|
||
for (int i = 0; i < nn; i++) fputs(lines[i], wf);
|
||
fprintf(wf, "language = %s\n", loc);
|
||
fclose(wf);
|
||
|
||
pmm_lang_set_locale(loc);
|
||
pmm_lang_load(out);
|
||
pmm_success("%s", pmm_tr_fmt("msg.lang-set", loc));
|
||
return 0;
|
||
}
|
||
|
||
/* 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];
|
||
/* A combined short flag cluster (e.g. -yq / -qy): consume the global
|
||
* chars (q=quiet, v=verbose); keep any non-global chars (e.g. y=yes)
|
||
* as a fresh "-y" token for the subcommand. */
|
||
if (a[0] == '-' && a[1] && a[1] != '-' && a[2]) {
|
||
char keep[64]; int ki = 0;
|
||
for (const char *p = a + 1; *p && ki < 62; p++) {
|
||
if (*p == 'q') { pmm_log_level = 1; continue; }
|
||
if (*p == 'v') { pmm_log_level = 2; continue; }
|
||
keep[ki++] = *p;
|
||
}
|
||
if (ki > 0) {
|
||
char buf[64]; buf[0] = '-'; memcpy(buf + 1, keep, ki); buf[ki + 1] = '\0';
|
||
argv[w++] = strdup(buf);
|
||
}
|
||
continue;
|
||
}
|
||
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
|
||
|
||
/* initialise i18n BEFORE any output (so `pmm` bare help is localized too).
|
||
* Use the configured language (if any), then load its pack (or built-in). */
|
||
{
|
||
PmmConfig cfg; load_config(&cfg);
|
||
if (cfg.language && *cfg.language) pmm_lang_set_locale(cfg.language);
|
||
free(cfg.registry_url); free(cfg.mirror_name); free(cfg.language);
|
||
pmm_lang_load_active();
|
||
}
|
||
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)\n", PMM_VERSION, pmm_detect_arch());
|
||
return 0;
|
||
}
|
||
if (strcmp(argv[1], "list") == 0) {
|
||
if (argc >= 3 && strcmp(argv[2], "--upgradable") == 0) return cmd_list_upgradable(argc - 2, argv + 2);
|
||
pdm_list_installed(); return 0;
|
||
}
|
||
if (strcmp(argv[1], "setting") == 0) return cmd_setting(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_c(PMM_E_USAGE, "用法: pmm remove <软件包名>", "%s", pmm_tr("msg.err.usage")); return 1; }
|
||
if (argv[2][0] == '-') { pmm_error_c(PMM_E_USAGE, "remove 后面应是包名而不是选项", "%s", pmm_tr_fmt("msg.err.unknown-cmd", argv[2])); 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);
|
||
/* If the running binary already matches the newest registry version,
|
||
* skip the (re)install. */
|
||
char *latest = registry_latest_version("pmm");
|
||
if (latest) {
|
||
if (cmp_version(PMM_VERSION, latest) >= 0) {
|
||
pmm_info("%s", pmm_tr_fmt("msg.self-up-to-date", PMM_VERSION, latest));
|
||
free(latest); free(cfg.registry_url); free(cfg.mirror_name);
|
||
free(mirror.name); free(mirror.api_base);
|
||
return 0;
|
||
}
|
||
free(latest);
|
||
}
|
||
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], "clean") == 0)
|
||
return cmd_clean();
|
||
|
||
if (strcmp(argv[1], "cache") == 0)
|
||
return cmd_cache(argc - 2, argv + 2);
|
||
|
||
if (strcmp(argv[1], "fetch") == 0)
|
||
return cmd_fetch(argc - 2, argv + 2);
|
||
|
||
if (strcmp(argv[1], "doctor") == 0 || strcmp(argv[1], "diagnose") == 0)
|
||
return cmd_doctor();
|
||
|
||
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_c(PMM_E_USAGE,
|
||
"用法: pmm install <软件包名|文件.deb|文件.msi|文件.rpm|文件.pdm> ...",
|
||
"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, "--offline") == 0 || strcmp(a, "-o") == 0) { pmm_offline = 1; continue; }
|
||
if (strcmp(a, "--force") == 0) { pmm_force_reinstall = 1; continue; }
|
||
if (strcmp(a, "-y") == 0 || strcmp(a, "--yes") == 0) { pmm_yes = 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) {
|
||
/* A bare leading option (e.g. `install --gits`) is not a package; report
|
||
* an unknown command instead of treating the flag as a name. */
|
||
if (argv[2][0] == '-')
|
||
pmm_error_c(PMM_E_USAGE, "install 后应跟软件包名, 或可识别的选项(如 --git)", "%s", pmm_tr_fmt("msg.err.unknown-cmd", argv[2]));
|
||
else items[ni++] = argv[2];
|
||
if (ni == 0) return 1;
|
||
}
|
||
|
||
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_c(PMM_E_INTERNAL, "安装失败, 可先 'pmm fetch <包名>' 手动下载排查", "%s", pmm_tr_fmt("msg.err.failed-install", 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_c(PMM_E_INTERNAL, "下载或安装失败, 检查网络/文件格式", "%s", pmm_tr_fmt("msg.err.failed-install", 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_c(PMM_E_INTERNAL, "安装失败, 检查文件是否为有效安装包", "%s", pmm_tr_fmt("msg.err.failed-install", 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_c(PMM_E_USAGE, "用法: pmm pack <目录> [输出.pdm]", "%s", pmm_tr("msg.err.usage"));
|
||
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_c(PMM_E_USAGE, "用法: pmm cache <list|clean>", "%s", pmm_tr("msg.err.usage")); 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_c(PMM_E_USAGE, "未知子命令, 用 'pmm help' 查看可用命令", "%s", pmm_tr_fmt("msg.err.unknown-cmd", argv[1]));
|
||
return 1;
|
||
}
|