feature: pmm upgrade + pmm info enhancement + color/quiet/verbose flags

- pmm upgrade [--yes]: scan ~/.pmm/installed/*.info, look up each package's
  latest registry version for this os/arch, and upgrade with a per-package
  prompt (or automatically with --yes). Adds cmp_version + installed_version
  helpers and wires the 'upgrade' subcommand.
- pmm info: also print package description (when present) and an indented
  versions list (or url/sha256 for legacy single-platform entries).
- Output flags: --no-color (also PMM_NO_COLOR=1), -q/--quiet (only warn+error),
  --verbose. Added pmm_no_color + pmm_log_level globals to out.c/out.h and a
  consume_global_flags() pre-dispatch pass in main.c so every subcommand honors
  them. info/success are suppressed only in quiet mode (verbose keeps them).
- help: document upgrade, --no-color, -q/--quiet, --verbose.

Verified on WSL gcc: compiles clean; no-color strips ANSI on tty, -q silences
info/success while keeping errors, --verbose keeps info, info lists description
+ versions, upgrade recognizes up-to-date packages.
这个提交包含在:
JGZYES
2026-09-04 20:13:42 +08:00
父节点 b51b0e0779
当前提交 51a88a4678
修改 3 个文件,包含 180 行新增3 行删除
+161 -2
查看文件
@@ -238,24 +238,159 @@ static int cmd_info(int argc, char **argv) {
const char *name = json_str(root, "name"); const char *name = json_str(root, "name");
const char *ver = json_str(root, "version"); const char *ver = json_str(root, "version");
pmm_info("%s%s%s\n", name ? name : pkg, ver ? " " : "", ver ? ver : ""); 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"); JsonValue *vr = json_get(root, "variants");
if (vr && vr->count > 0) { if (vr && vr->count > 0) {
printf(" versions:\n");
for (int i = 0; i < vr->count; i++) { for (int i = 0; i < vr->count; i++) {
JsonValue *v = json_at(vr, i); JsonValue *v = json_at(vr, i);
if (!v) continue; if (!v) continue;
const char *vv = json_str(v, "version"); const char *vv = json_str(v, "version");
const char *osn = json_str(v, "os"); const char *osn = json_str(v, "os");
const char *archn = json_str(v, "arch"); const char *archn = json_str(v, "arch");
printf(" %s %s/%s\n", vv ? vv : "?", osn ? osn : "-", archn ? archn : "-"); printf(" %-12s %s/%s\n", vv ? vv : "?", osn ? osn : "-", archn ? archn : "-");
} }
} else { } else {
const char *u = json_str(root, "url"); const char *u = json_str(root, "url");
const char *s256 = json_str(root, "sha256");
printf(" url: %s\n", u ? u : "-"); printf(" url: %s\n", u ? u : "-");
if (s256) printf(" sha256: %s\n", s256);
} }
json_free(root); json_free(root);
return 0; 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");
}
json_free(root);
if (!best) continue;
if (cmp_version(best, curver) <= 0) { pmm_info("%s %s is up to date (latest %s)\n", pkg, curver, best); continue; }
pmm_info("%s: %s -> %s\n", pkg, curver, best);
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;
}
/* pmm verify <file> — print sha256 (and sha1) of a downloaded package file. */ /* pmm verify <file> — print sha256 (and sha1) of a downloaded package file. */
static int cmd_verify(int argc, char **argv) { static int cmd_verify(int argc, char **argv) {
if (argc < 1) { pmm_error("usage: pmm verify <file>\n"); return 1; } if (argc < 1) { pmm_error("usage: pmm verify <file>\n"); return 1; }
@@ -485,6 +620,7 @@ static void print_help(void) {
printf(" pmm list list installed packages\n"); printf(" pmm list list installed packages\n");
printf(" pmm remove <pkg> uninstall a package\n"); printf(" pmm remove <pkg> uninstall a package\n");
printf(" pmm self-update update pmm (auto os/arch)\n"); printf(" pmm self-update update pmm (auto os/arch)\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 --git <repo-url.git> any git host, API auto-detected\n");
printf(" pmm install --github <owner/repo> GitHub latest release\n"); printf(" pmm install --github <owner/repo> GitHub latest release\n");
printf(" pmm install --gitlab <owner/repo> GitLab latest release\n"); printf(" pmm install --gitlab <owner/repo> GitLab latest release\n");
@@ -495,7 +631,10 @@ static void print_help(void) {
printf(" pmm version | help\n\n"); printf(" pmm version | help\n\n");
printf("options:\n"); printf("options:\n");
printf(" -p<drive> install under <DRIVE>:\\\\.pmm (e.g. -pd -> D:\\\\.pmm, -pc -> C:\\\\.pmm)\n"); printf(" -p<drive> install under <DRIVE>:\\\\.pmm (e.g. -pd -> D:\\\\.pmm, -pc -> C:\\\\.pmm)\n");
printf(" 第一个 -p 会被记住,后续命令无需再写;用 -pc 切回 C 盘\n\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("config: <base>/pmm.json | pmm.ini | pmm.conf (base = <drive>:\\\\.pmm 或 ~/.pmm)\n");
printf("mirrors: <base>/mirror.ini | mirror.conf\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"); printf("asset mapping: windows=exe/msi/zip/7z linux=deb/rpm/appimage/tar.* macos=dmg/pkg\n");
@@ -531,6 +670,22 @@ static int consume_drive_flag(int argc, char **argv) {
return w; 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) { int main(int argc, char **argv) {
#ifdef _WIN32 #ifdef _WIN32
SetConsoleOutputCP(CP_UTF8); /* UTF-8 output so Chinese help/version isn't mojibake */ SetConsoleOutputCP(CP_UTF8); /* UTF-8 output so Chinese help/version isn't mojibake */
@@ -554,6 +709,7 @@ int main(int argc, char **argv) {
if (argc < 2) { print_help(); return 0; } if (argc < 2) { print_help(); return 0; }
pmm_set_self_path(argv[0]); pmm_set_self_path(argv[0]);
argc = consume_drive_flag(argc, argv); argc = consume_drive_flag(argc, argv);
argc = consume_global_flags(argc, argv);
if (argc < 2) { print_help(); return 0; } if (argc < 2) { print_help(); return 0; }
if (strcmp(argv[1], "help") == 0 || strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-h") == 0) { if (strcmp(argv[1], "help") == 0 || strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-h") == 0) {
@@ -586,6 +742,9 @@ int main(int argc, char **argv) {
return rc == 0 ? 0 : 1; return rc == 0 ? 0 : 1;
} }
if (strcmp(argv[1], "upgrade") == 0)
return cmd_upgrade(argc - 2, argv + 2);
if (strcmp(argv[1], "install") == 0) { if (strcmp(argv[1], "install") == 0) {
if (argc >= 3 && (strcmp(argv[2], "--git") == 0 || strcmp(argv[2], "--github") == 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], "--gitlab") == 0 || strcmp(argv[2], "--gitea") == 0 ||
+7 -1
查看文件
@@ -12,6 +12,10 @@
#define IS_TTY_FD(fd) isatty(fd) #define IS_TTY_FD(fd) isatty(fd)
#endif #endif
/* Global output flags (extern in out.h). Set from main.c flags / env. */
int pmm_no_color = 0; /* --no-color / PMM_NO_COLOR=1: never emit ANSI */
int pmm_log_level = 0; /* 0 normal, 1 quiet (-q), 2 verbose (--verbose) */
/* Emit a message with an optional ANSI colour and a [PMM]:[LEVEL] prefix. /* Emit a message with an optional ANSI colour and a [PMM]:[LEVEL] prefix.
* fd_stream: 1 for stdout (info/success), 2 for stderr (error/warn). * fd_stream: 1 for stdout (info/success), 2 for stderr (error/warn).
* colour is the ANSI SGR number (0 = no colour), OR "" style. We always write * colour is the ANSI SGR number (0 = no colour), OR "" style. We always write
@@ -27,7 +31,7 @@ static void pmmsg(FILE *stream, int fd, const char *level, int wrap_color, const
size_t len = strlen(buf); size_t len = strlen(buf);
while (len && (buf[len-1] == '\n' || buf[len-1] == '\r')) buf[--len] = '\0'; while (len && (buf[len-1] == '\n' || buf[len-1] == '\r')) buf[--len] = '\0';
int tty = IS_TTY_FD(fd); int tty = IS_TTY_FD(fd) && !pmm_no_color;
if (tty && wrap_color) if (tty && wrap_color)
fprintf(stream, "\033[%dm[PMM]:[%s]%s\033[0m\n", wrap_color, level, buf); fprintf(stream, "\033[%dm[PMM]:[%s]%s\033[0m\n", wrap_color, level, buf);
else else
@@ -41,12 +45,14 @@ void pmm_error(const char *fmt, ...) {
} }
void pmm_success(const char *fmt, ...) { void pmm_success(const char *fmt, ...) {
if (pmm_log_level == 1) return; /* quiet: suppress success; verbose(2) keeps it */
va_list ap; va_start(ap, fmt); va_list ap; va_start(ap, fmt);
pmmsg(stdout, 1, "SUCCESS", 32, fmt, ap); /* green */ pmmsg(stdout, 1, "SUCCESS", 32, fmt, ap); /* green */
va_end(ap); va_end(ap);
} }
void pmm_info(const char *fmt, ...) { void pmm_info(const char *fmt, ...) {
if (pmm_log_level == 1) return; /* quiet: suppress info; verbose(2) keeps it */
va_list ap; va_start(ap, fmt); va_list ap; va_start(ap, fmt);
pmmsg(stdout, 1, "INFO", 2, fmt, ap); /* grey (SGR 2 = dim) */ pmmsg(stdout, 1, "INFO", 2, fmt, ap); /* grey (SGR 2 = dim) */
va_end(ap); va_end(ap);
+12
查看文件
@@ -13,6 +13,18 @@
#ifndef PMM_OUT_H #ifndef PMM_OUT_H
#define PMM_OUT_H #define PMM_OUT_H
#include <stddef.h>
/* Global output flags consulted by the pmm_* helpers.
* pmm_no_color : 1 = never emit ANSI colour (even on a tty). Set by --no-color
* or PMM_NO_COLOR=1.
* pmm_log_level : 0 = normal (INFO+SUCCESS+WARN+ERROR prints).
* 1 = quiet (-q/--quiet): only WARN and ERROR print.
* 2 = verbose (--verbose): INFO/SUCCESS print plus extra debug.
*/
extern int pmm_no_color;
extern int pmm_log_level;
void pmm_error(const char *fmt, ...); void pmm_error(const char *fmt, ...);
void pmm_success(const char *fmt, ...); void pmm_success(const char *fmt, ...);
void pmm_info(const char *fmt, ...); void pmm_info(const char *fmt, ...);