feat: real dependency resolver + pmm clean + self-update version check

Dependency resolver (install.c):
- Parse name (op ver) ranges; reuse spec_match for version checking.
- installed_satisfies(): skip a dep already satisfied by an installed package
  (own name or an installed Provides), matching the version constraint - no more
  needless re-installs.
- any_installed_conflict(): reject an install whose Conflicts: list names
  something already installed (checked in pdm_install_file before extracting).
- Topological order stays dep-first via recursion + dep_seen dedup set (deps are
  installed before the parent package, diamonds installed once).

pmm clean (main.c):
- New command clears the cache dir (~/.pmm/cache incl. registry snapshots),
  keeps the dir, reports files/size freed.

self-update (main.c):
- Before installing, compare PMM_VERSION against the registry's top-level
  'version' (mirror bumps it each release); skip with 'already up to date' when
  current >= latest (avoids a pointless re-download/install).

Also: i18n keys for the new messages; help text lists pmm clean.
这个提交包含在:
JGZYES
2026-09-05 18:25:32 +08:00
父节点 37dd970209
当前提交 f30d87c2b5
修改 5 个文件,包含 228 行新增3 行删除
+3 -3
查看文件
@@ -53,16 +53,16 @@ static const char *kBuiltinZh =
"\"msg.hash-sha256\":\"sha256: %s\",\"msg.hash-sha1\":\"sha1: %s\"," "\"msg.hash-sha256\":\"sha256: %s\",\"msg.hash-sha1\":\"sha1: %s\","
"\"msg.installing\":\"正在安装 %s ...\",\"msg.installed\":\"已安装 %s\"," "\"msg.installing\":\"正在安装 %s ...\",\"msg.installed\":\"已安装 %s\","
"\"msg.checksum-ok\":\"%s 校验 OK: %s\",\"msg.checksum-mismatch\":\"校验不匹配 (%s)!\"," "\"msg.checksum-ok\":\"%s 校验 OK: %s\",\"msg.checksum-mismatch\":\"校验不匹配 (%s)!\","
"\"msg.resolving-dep\":\"正在解析依赖 %s\",\"msg.registry-ok\":\"注册表 sha256 OK: %s\"," "\"msg.resolving-dep\":\"正在解析依赖 %s%s%s\",\"msg.dep-satisfied\":\"依赖 %s%s%s 已装/被提供,跳过\",\"msg.registry-ok\":\"注册表 sha256 OK: %s\",\"msg.cache-cleared\":\"已清理缓存: %d 个文件,释放 %.1f KB\",\"msg.self-up-to-date\":\"当前 v%s 已是最新(v%s),跳过升级\","
"\"msg.path-exists\":\"PATH 已包含 PMM 目录\",\"msg.err.registry-not-found\":\"找不到软件包 '%s'\"," "\"msg.path-exists\":\"PATH 已包含 PMM 目录\",\"msg.err.registry-not-found\":\"找不到软件包 '%s'\","
"\"msg.pmm-self-replaced\":\"pmm 已更新到 %s\",\"msg.pmm-self-hint\":\"无法覆盖 %s(需 root/管理员);新版本在 %s。请将其加入 PATH 并在当前 shell 运行 hash -r\"," "\"msg.pmm-self-replaced\":\"pmm 已更新到 %s\",\"msg.pmm-self-hint\":\"无法覆盖 %s(需 root/管理员);新版本在 %s。请将其加入 PATH 并在当前 shell 运行 hash -r\","
"\"msg.err.usage\":\"用法\",\"msg.err.unknown-cmd\":\"未知命令 '%s' (试试 'pmm help')\"," "\"msg.err.usage\":\"用法\",\"msg.err.unknown-cmd\":\"未知命令 '%s' (试试 'pmm help')\","
"\"msg.err.download-failed\":\"下载语言包 %s 失败\",\"msg.err.invalid-locale\":\"无效语言 %s\"," "\"msg.err.download-failed\":\"下载语言包 %s 失败\",\"msg.err.invalid-locale\":\"无效语言 %s\","
"\"msg.err.not-found\":\"找不到软件包 '%s'\",\"msg.err.failed-install\":\"安装 %s 失败\"," "\"msg.err.not-found\":\"找不到软件包 '%s'\",\"msg.err.failed-install\":\"安装 %s 失败\",\"msg.err.conflict\":\"无法安装 %s:与已装的 %s 冲突\","
"\"msg.err.no-registry-mirror\":\"没有配置注册表镜像\",\"msg.err.cannot-write\":\"无法写入 %s\"," "\"msg.err.no-registry-mirror\":\"没有配置注册表镜像\",\"msg.err.cannot-write\":\"无法写入 %s\","
"\"desc.install\":\"安装\",\"desc.remove\":\"卸载\",\"desc.update\":\"刷新索引\"," "\"desc.install\":\"安装\",\"desc.remove\":\"卸载\",\"desc.update\":\"刷新索引\","
"\"desc.upgrade\":\"升级\",\"desc.mirror\":\"镜像源\",\"desc.search\":\"搜索\"," "\"desc.upgrade\":\"升级\",\"desc.mirror\":\"镜像源\",\"desc.search\":\"搜索\","
"\"desc.info\":\"详情\",\"desc.self-update\":\"更新自身\",\"desc.help\":\"帮助\"," "\"desc.info\":\"详情\",\"desc.self-update\":\"更新自身\",\"desc.clean\":\"清理缓存\",\"desc.help\":\"帮助\","
"\"opt.p-drive\":\"安装盘符\",\"opt.no-color\":\"禁用颜色\",\"opt.quiet\":\"静默\"," "\"opt.p-drive\":\"安装盘符\",\"opt.no-color\":\"禁用颜色\",\"opt.quiet\":\"静默\","
"\"opt.verbose\":\"详情\"}"; "\"opt.verbose\":\"详情\"}";
+127
查看文件
@@ -911,9 +911,136 @@ int install_from_registry(const char *name, const char *spec, const char *mirror
return rc; return rc;
} }
/* ---------- installed-package DB queries (dependency solving) ---------- */
/* Extract the first value of `key` from a control/.info text buffer. Returns a
* malloc'd copy or NULL. Multi-value fields (Depends/Provides/Conflicts) are
* returned as their single comma-separated line. */
static char *ctl_field(const char *ctl, const char *key) {
size_t klen = strlen(key);
const char *line = ctl;
while (line && *line) {
const char *eol = strchr(line, '\n');
size_t ll = eol ? (size_t)(eol - line) : strlen(line);
if (ll > klen && strncasecmp(line, key, klen) == 0 && line[klen] == ':') {
const char *v = line + klen + 1;
while (v < line + ll && (*v == ' ' || *v == '\t')) v++;
size_t vl = (size_t)(line + ll - v);
while (vl && (v[vl-1] == ' ' || v[vl-1] == '\r') ) vl--;
char *out = malloc(vl + 1);
if (!out) return NULL;
memcpy(out, v, vl); out[vl] = '\0';
return out;
}
line = eol ? eol + 1 : NULL;
}
return NULL;
}
static char *read_text_file(const char *path) {
FILE *f = fopen(path, "rb");
if (!f) return NULL;
fseek(f, 0, SEEK_END); long n = ftell(f); fseek(f, 0, SEEK_SET);
if (n < 0) { fclose(f); return NULL; }
char *buf = malloc((size_t)n + 1);
if (!buf) { fclose(f); return NULL; }
size_t rd = fread(buf, 1, (size_t)n, f); fclose(f);
buf[rd] = '\0';
return buf;
}
/* Comma/space-separated Provides or Conflicts list contains `name`? */
static int field_has(const char *fieldval, const char *name) {
if (!fieldval || !*fieldval) return 0;
size_t nl = strlen(name);
const char *p = fieldval;
while (*p) {
while (*p == ' ' || *p == '\t' || *p == ',') p++;
const char *s = p;
while (*p && *p != ',' && *p != ' ' && *p != '\t') p++;
if ((size_t)(p - s) == nl && strncasecmp(s, name, nl) == 0) return 1;
}
return 0;
}
/* Does an installed package provide `name` (as its own Package, or via a
* Provides: entry)? If found, store its installed Version into `ver_out`.
* Returns 1 if `name` is satisfied by something installed, else 0. */
static int installed_provides(const char *name, char *ver_out, size_t vs) {
char home[1024];
pmm_config_dir(home, sizeof(home));
char db[1200];
snprintf(db, sizeof(db), "%s/installed", home);
(void)ver_out; (void)vs;
#ifndef _WIN32
DIR *d = opendir(db);
if (!d) return 0;
int hit = 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;
char path[1400];
snprintf(path, sizeof(path), "%s/%s", db, e->d_name);
char *info = read_text_file(path);
if (!info) continue;
char *pkg = ctl_field(info, "Package");
char *ver = ctl_field(info, "Version");
char *prov = ctl_field(info, "Provides");
if (pkg && strcmp(pkg, name) == 0) {
if (ver_out && ver) snprintf(ver_out, vs, "%s", ver);
hit = 1;
} else if (field_has(prov, name)) {
if (ver_out && ver) snprintf(ver_out, vs, "%s", ver);
hit = 1;
}
free(pkg); free(ver); free(prov); free(info);
if (hit) break;
}
closedir(d);
return hit;
#else
return 0; /* conservative: never claim a dep is satisfied on Windows */
#endif
}
/* Is the requested dep (name + optional version spec) already satisfied by an
* installed package (own name or a Provides), matching the version range?
* Returns 1 = satisfied (skip install), 0 = must install. */
static int installed_satisfies(const char *depName, const char *spec) {
char ver[128] = "";
if (!installed_provides(depName, ver, sizeof(ver)) || !ver[0]) return 0;
if (!spec || !*spec) return 1;
return spec_match(ver, spec) ? 1 : 0;
}
/* Does `conflicts` (a comma list) name anything already installed? Used to
* reject an install that would clash with the current environment. */
int any_installed_conflict(const char *conflicts) {
if (!conflicts || !*conflicts) return 0;
char *dup = strdup(conflicts), *p = dup;
int hit = 0;
while (p && *p) {
char *end = strchr(p, ',');
if (end) *end = '\0';
char *t = p; while (*t == ' ' || *t == '\t') t++;
if (*t && installed_provides(t, NULL, 0)) { hit = 1; break; }
if (!end) break;
p = end + 1;
}
free(dup);
return hit;
}
static int install_dep_spec(const char *depstr) { static int install_dep_spec(const char *depstr) {
char name[256], spec[256]; char name[256], spec[256];
if (parse_dep(depstr, name, sizeof(name), spec, sizeof(spec)) != 0) return -1; if (parse_dep(depstr, name, sizeof(name), spec, sizeof(spec)) != 0) return -1;
/* If this dep is already satisfied by an installed package (its own
* name or a Provides), don't re-install it. */
if (installed_satisfies(name, spec)) {
pmm_info("%s", pmm_tr_fmt("msg.dep-satisfied", name, spec[0] ? " " : "", spec));
return 0;
}
for (int i = 0; i < dep_seen_n; i++) for (int i = 0; i < dep_seen_n; i++)
if (strcmp(dep_seen[i], name) == 0) return 0; /* cycle / duplicate */ if (strcmp(dep_seen[i], name) == 0) return 0; /* cycle / duplicate */
if (dep_seen_n < 128) dep_seen[dep_seen_n++] = strdup(name); if (dep_seen_n < 128) dep_seen[dep_seen_n++] = strdup(name);
+4
查看文件
@@ -21,6 +21,10 @@ extern int pmm_yes;
/* Resolve a comma-separated Depends list ("a (>= 1.0), b") from the registry. */ /* Resolve a comma-separated Depends list ("a (>= 1.0), b") from the registry. */
int pmm_install_dep_list(const char *list); int pmm_install_dep_list(const char *list);
/* Does a comma-separated Conflicts list name anything already installed?
* (dependency solver helper, also called from the .pdm installer). */
int any_installed_conflict(const char *conflicts);
/* Install from a remote registry (apt-style multi-mirror fallback): /* Install from a remote registry (apt-style multi-mirror fallback):
* looks up package `name` in the configured registry mirrors by priority. * looks up package `name` in the configured registry mirrors by priority.
* If `version` is non-NULL, fetches {registry}/{name}/{version}.json, otherwise * If `version` is non-NULL, fetches {registry}/{name}/{version}.json, otherwise
+85
查看文件
@@ -809,6 +809,75 @@ static int cmd_mirror(int argc, char **argv) {
return 1; 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++;
RemoveFileA(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;
}
/* 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;
}
static void print_help(void) { static void print_help(void) {
printf("PMM %s (%s)\n\n", PMM_VERSION, pmm_detect_arch()); printf("PMM %s (%s)\n\n", PMM_VERSION, pmm_detect_arch());
printf("%s\n", pmm_tr("help.usage")); printf("%s\n", pmm_tr("help.usage"));
@@ -820,6 +889,7 @@ static void print_help(void) {
printf(" %-32s%s\n", "pmm upgrade [--yes] ", pmm_tr("desc.upgrade")); printf(" %-32s%s\n", "pmm upgrade [--yes] ", pmm_tr("desc.upgrade"));
printf(" %-32s%s\n", "pmm mirror ... ", pmm_tr("desc.mirror")); printf(" %-32s%s\n", "pmm mirror ... ", pmm_tr("desc.mirror"));
printf(" %-32s%s\n", "pmm self-update ", pmm_tr("desc.self-update")); 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 version | help ", pmm_tr("desc.help")); printf(" %-32s%s\n", "pmm version | help ", pmm_tr("desc.help"));
printf("\n%s\n", pmm_tr("help.options")); printf("\n%s\n", pmm_tr("help.options"));
printf(" %-32s%s\n", "-p<drive> ", pmm_tr("opt.p-drive")); printf(" %-32s%s\n", "-p<drive> ", pmm_tr("opt.p-drive"));
@@ -1099,6 +1169,18 @@ int main(int argc, char **argv) {
PmmConfig cfg; MirrorSel mirror; PmmConfig cfg; MirrorSel mirror;
load_config(&cfg); load_config(&cfg);
load_mirror(&cfg, &mirror); 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_info("self-update -> installing latest pmm (%s/%s)\n",
pmm_os_name(pmm_detect_os()), pmm_detect_arch()); pmm_os_name(pmm_detect_os()), pmm_detect_arch());
int rc = install_from_registry("pmm", NULL, mirror.name); int rc = install_from_registry("pmm", NULL, mirror.name);
@@ -1110,6 +1192,9 @@ int main(int argc, char **argv) {
return rc == 0 ? 0 : 1; return rc == 0 ? 0 : 1;
} }
if (strcmp(argv[1], "clean") == 0)
return cmd_clean();
if (strcmp(argv[1], "update") == 0) if (strcmp(argv[1], "update") == 0)
return cmd_update(); return cmd_update();
+9
查看文件
@@ -540,6 +540,15 @@ int pdm_install_file(const char *pdmfile) {
chdir_restore(); remove(tmpname); return -1; chdir_restore(); remove(tmpname); return -1;
} }
/* reject an install that would conflict with a package already present */
char *confl = control_get(ctl, "Conflicts");
if (confl && *confl && any_installed_conflict(confl)) {
pmm_error("%s", pmm_tr_fmt("msg.err.conflict", pkg, confl));
free(confl); free(pkg); if (ver) free(ver);
chdir_restore(); remove(tmpname); return -1;
}
free(confl);
/* extract data into root (relative target: cwd is already the pm dir) */ /* extract data into root (relative target: cwd is already the pm dir) */
const char *tgt = flat ? "." : "root"; const char *tgt = flat ? "." : "root";
/* If the package is pmm itself, its exe is the currently running binary and /* If the package is pmm itself, its exe is the currently running binary and