diff --git a/CHANGELOG/0.3.6.md b/CHANGELOG/0.3.6.md new file mode 100644 index 0000000..30398c7 --- /dev/null +++ b/CHANGELOG/0.3.6.md @@ -0,0 +1,15 @@ +# PMM v0.3.6 + +## 新增 +- `pmm list --upgradable`:列出有更新版本的已安装软件包(只展示,不升级)。 +- `pmm install --force`:强制重装。 +- `pmm setting set ` / `pmm setting get`:通用配置读写(写 `pmm.conf`)。 +- `pmm setting mirror `:快捷设置当前镜像。 +- `install` 支持 `-y/--yes`(配合后续确认场景)。 + +## 修复 +- Windows 上 `pmm install pmm` 自升级失败(运行中的 exe 被锁定):解压前把运行的 pmm.exe 退位, + 写入新版本后再清理,避免 `Can't unlink already-existing object: Permission denied`。 + +## 说明 +- list --upgradable 会联网查询各包最新版。 diff --git a/src/install.c b/src/install.c index 24af90c..4539ddd 100644 --- a/src/install.c +++ b/src/install.c @@ -156,6 +156,8 @@ static int native_binary_ok(const char *path, PmmOS os) { static int install_path(const char *path, const char *name); int pmm_no_cache = 0; /* --no-cache: drop cached file before download */ +int pmm_force_reinstall = 0; /* --force: reinstall even if already present */ +int pmm_yes = 0; /* -y/--yes: skip confirmation prompts */ /* Extract the data.tar.* member of a .deb (an ar container) to `outdata`. * Pure C (no ar/dpkg required). Returns compression code: diff --git a/src/install.h b/src/install.h index 1f6be3a..32a86dc 100644 --- a/src/install.h +++ b/src/install.h @@ -13,6 +13,11 @@ int install_local_file(const char *path); /* Set by `pmm install --no-cache` to force a fresh download (drop cache file). */ extern int pmm_no_cache; +/* Set by `pmm install --force` to reinstall even if a package is already + * present, and by `-y/--yes` to skip any confirmation prompt. */ +extern int pmm_force_reinstall; +extern int pmm_yes; + /* Resolve a comma-separated Depends list ("a (>= 1.0), b") from the registry. */ int pmm_install_dep_list(const char *list); diff --git a/src/main.c b/src/main.c index 416bebd..318e446 100644 --- a/src/main.c +++ b/src/main.c @@ -439,6 +439,74 @@ static int cmd_upgrade(int argc, char **argv) { 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/. Returns 0 on success. */ static int save_registry_cache(const char *rel, const char *body) { char cache[1024]; @@ -772,12 +840,61 @@ static void mkdir_p_local(char *path) { /* pmm setting lang ... */ static int cmd_setting(int argc, char **argv) { - if (argc < 1 || strcmp(argv[0], "lang") != 0) { + if (argc < 1) { pmm_error("%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 — persist a key=value in pmm.conf */ + if (strcmp(argv[0], "set") == 0) { + if (argc < 3) { pmm_error("%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("%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("%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 — set active mirror (persist mirror=) */ + if (strcmp(argv[0], "mirror") == 0) { + if (argc < 2) { pmm_error("%s", pmm_tr("msg.err.usage")); return 1; } + /* reuse setting set mirror */ + char *fake[] = { (char*)"set", (char*)"mirror", argv[1], NULL }; + return cmd_setting(3, fake); + } + + if (strcmp(argv[0], "lang") != 0) { + pmm_error("%s", pmm_tr("msg.err.usage")); + return 1; + } char langdir[1200]; snprintf(langdir, sizeof(langdir), "%s/lang", dir); @@ -829,8 +946,6 @@ static int cmd_setting(int argc, char **argv) { mirrors_free(ml); /* persist language= in pmm.conf (always-writable text) */ - char cfgpath[1200]; - snprintf(cfgpath, sizeof(cfgpath), "%s/pmm.conf", dir); char lines[256][1024]; int nn = 0; FILE *rf = fopen(cfgpath, "r"); if (rf) { @@ -955,7 +1070,10 @@ int main(int argc, char **argv) { pmm_info("PMM %s (%s)\n", PMM_VERSION, pmm_detect_arch()); return 0; } - if (strcmp(argv[1], "list") == 0) { pdm_list_installed(); 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], "mirror") == 0) return cmd_mirror(argc - 2, argv + 2); if (strcmp(argv[1], "setting") == 0) return cmd_setting(argc - 2, argv + 2); /* pmm remove (also used by the Windows "Uninstall" registration) */ @@ -1005,6 +1123,8 @@ int main(int argc, char **argv) { 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, "--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; } diff --git a/web/_common.php b/web/_common.php index f26821e..9670e85 100644 --- a/web/_common.php +++ b/web/_common.php @@ -4,7 +4,7 @@ * then pmm_header($active,$title); ... pmm_footer(); */ if (!defined('PMM_SITE')) { http_response_code(403); exit('forbidden'); } -define('PMM_VERSION', '0.3.5'); +define('PMM_VERSION', '0.3.6'); function pmm_nav_links(string $active): string { $items = [ diff --git a/web/install.sh b/web/install.sh index 6f11cd2..a8d4e8c 100644 --- a/web/install.sh +++ b/web/install.sh @@ -7,7 +7,7 @@ # # 下载官方 release 二进制并安装到 /usr/local/bin/pmm(root)或 ~/.local/bin/pmm。 set -e -VER="0.3.5" +VER="0.3.6" REPO="JGZYES/ParlzPackageManger" # ---- 检测平台 ---- diff --git a/web/mirror/lang/en-US.pjson b/web/mirror/lang/en-US.pjson index d9a6a30..1944ca4 100644 --- a/web/mirror/lang/en-US.pjson +++ b/web/mirror/lang/en-US.pjson @@ -70,6 +70,7 @@ "msg.mirror-active": "active mirror set to '%s'", "msg.mirror-removed": "mirror '%s' removed", "msg.lang-set": "language set to '%s'", + "msg.setting-set": "set %s = %s", "msg.empty-path": "empty file path", "msg.verifying-native": "verifying %s is a native %s binary...", "msg.resolving-dep": "resolving dependency %s%s%s", diff --git a/web/mirror/lang/zh-CN.pjson b/web/mirror/lang/zh-CN.pjson index 8b135e8..3282e57 100644 --- a/web/mirror/lang/zh-CN.pjson +++ b/web/mirror/lang/zh-CN.pjson @@ -70,6 +70,7 @@ "msg.mirror-active": "当前镜像源已设为 '%s'", "msg.mirror-removed": "镜像源 '%s' 已移除", "msg.lang-set": "语言已设为 '%s'", + "msg.setting-set": "已设置 %s = %s", "msg.empty-path": "文件路径为空", "msg.verifying-native": "正在验证 %s 是否为可用的 %s 二进制...", "msg.resolving-dep": "正在解析依赖 %s%s%s",