diff --git a/src/i18n.c b/src/i18n.c index fe8a8b4..5a5da6f 100644 --- a/src/i18n.c +++ b/src/i18n.c @@ -53,16 +53,16 @@ static const char *kBuiltinZh = "\"msg.hash-sha256\":\"sha256: %s\",\"msg.hash-sha1\":\"sha1: %s\"," "\"msg.installing\":\"正在安装 %s ...\",\"msg.installed\":\"已安装 %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.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.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\"," "\"desc.install\":\"安装\",\"desc.remove\":\"卸载\",\"desc.update\":\"刷新索引\"," "\"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.verbose\":\"详情\"}"; diff --git a/src/install.c b/src/install.c index 4539ddd..cb7f5c5 100644 --- a/src/install.c +++ b/src/install.c @@ -911,9 +911,136 @@ int install_from_registry(const char *name, const char *spec, const char *mirror 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) { char name[256], spec[256]; 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++) if (strcmp(dep_seen[i], name) == 0) return 0; /* cycle / duplicate */ if (dep_seen_n < 128) dep_seen[dep_seen_n++] = strdup(name); diff --git a/src/install.h b/src/install.h index 32a86dc..adcf381 100644 --- a/src/install.h +++ b/src/install.h @@ -21,6 +21,10 @@ 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); +/* 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): * looks up package `name` in the configured registry mirrors by priority. * If `version` is non-NULL, fetches {registry}/{name}/{version}.json, otherwise diff --git a/src/main.c b/src/main.c index aa9634f..5a192f0 100644 --- a/src/main.c +++ b/src/main.c @@ -809,6 +809,75 @@ static int cmd_mirror(int argc, char **argv) { 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 `.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) { printf("PMM %s (%s)\n\n", PMM_VERSION, pmm_detect_arch()); 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 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 version | help ", pmm_tr("desc.help")); printf("\n%s\n", pmm_tr("help.options")); printf(" %-32s%s\n", "-p ", pmm_tr("opt.p-drive")); @@ -1099,6 +1169,18 @@ int main(int argc, char **argv) { 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); @@ -1110,6 +1192,9 @@ int main(int argc, char **argv) { return rc == 0 ? 0 : 1; } + if (strcmp(argv[1], "clean") == 0) + return cmd_clean(); + if (strcmp(argv[1], "update") == 0) return cmd_update(); diff --git a/src/pdm.c b/src/pdm.c index 2d3ce1e..bd75bf4 100644 --- a/src/pdm.c +++ b/src/pdm.c @@ -540,6 +540,15 @@ int pdm_install_file(const char *pdmfile) { 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) */ const char *tgt = flat ? "." : "root"; /* If the package is pmm itself, its exe is the currently running binary and