diff --git a/CHANGELOG/0.6.0.md b/CHANGELOG/0.6.0.md new file mode 100644 index 0000000..0946b40 --- /dev/null +++ b/CHANGELOG/0.6.0.md @@ -0,0 +1,27 @@ +# PMM v0.6.0 + +## 新增 +- **错误码系统(结构化报错)**:新增 `pmm_err.h` 定义稳定的错误码枚举 + `PMM_E_*`(`PMM_E_USAGE=1`、`PMM_E_NOT_FOUND=2`、`PMM_E_DOWNLOAD=3`、 + `PMM_E_NETWORK=4`、`PMM_E_NO_MIRROR=5`、`PMM_E_REGISTRY=6`、`PMM_E_CHECKSUM=7`、 + `PMM_E_EXTRACT=8`、`PMM_E_CONFLICT=9`、`PMM_E_NO_TAR=10`、`PMM_E_NO_ROOT=11`、 + `PMM_E_CACHE=12`、`PMM_E_LANG=13`、`PMM_E_INTERNAL=99`),每个报错都可以被 + 脚本 / 工具 / 用户可靠识别。 +- **`pmm_error_c(code, hint, fmt, ...)`**:统一输出 + `[PMM]:[E]: <消息> (hint: <解决办法>)`,每条错误都带**回答**(怎么修)。 +- **高频报错点全部接入错误码**:`main.c` / `pdm.c` / `install.c` / `json.c` / + `ppdm.c` 的用法、找不到、下载失败、校验和、解包、冲突、缺镜像、缺 tar、 + 权限、缓存、语言包等报错统一走 `pmm_error_c`。 + +## 修复 +- **杜绝裸/空报错**: + - `pmm_error` 在消息为空时不再输出 `[PMM]:[ERROR]:`(兜底为 `(no message)`)。 + - `pmm_tr` 增加“互补内置包回退”:中文内置表是英文全表的子集,缺键时再回退到 + 英文内置表,任何 `msg.xxx` 键都不会裸显示原始 key。 +- 报错格式统一为 `[PMM]:[E]: 消息 (hint: 提示)`,颜色仅在有 TTY 时包裹, + 日志/管道下输出纯文本,方便机器解析。 + +## 说明 +- 兼容性:错误码在 0.6.0 起稳定;旧脚本若匹配 `[PMM]:[ERROR]:` 前缀,建议改用 + `[PMM]:[E]:`(`` 为两位数字错误码)。 +- `ppdm` 仍为独立版本 `0.0.1`,本次同步接入错误码格式。 diff --git a/src/i18n.c b/src/i18n.c index 1332cc8..042803c 100644 --- a/src/i18n.c +++ b/src/i18n.c @@ -9,6 +9,7 @@ static JsonValue *g_lang = NULL; static JsonValue *g_builtin = NULL; +static JsonValue *g_builtin_alt = NULL; static char g_locale[64] = ""; /* Detect locale from LANG / LC_ALL env, else default "zh-CN". */ @@ -92,8 +93,10 @@ int pmm_lang_load_active(void) { } const char *pmm_tr(const char *key) { - /* Look up in the loaded pack first, then fall back to the built-in zh-CN - * table so a stale .pjson never shows a raw "msg.xxx" key. */ + /* Look up in the loaded pack first, then the locale-matched built-in, then + * the *other* built-in. The zh table is a small subset of the full en table, + * so without this last fallback a stale pack or a short zh table would leak + * a raw "msg.xxx" key. */ if (g_lang) { const char *t = json_str(g_lang, key); if (t) return t; @@ -103,6 +106,12 @@ const char *pmm_tr(const char *key) { const char *b = json_str(g_builtin, key); if (b) return b; } + if (!g_builtin_alt) + g_builtin_alt = json_parse((builtin_source() == kBuiltinEn) ? kBuiltinZh : kBuiltinEn); + if (g_builtin_alt) { + const char *b = json_str(g_builtin_alt, key); + if (b) return b; + } return key; } diff --git a/src/install.c b/src/install.c index eb5a7ae..07a0452 100644 --- a/src/install.c +++ b/src/install.c @@ -14,6 +14,7 @@ #include "install.h" #include "pmm.h" #include "out.h" +#include "pmm_err.h" #include "i18n.h" #include "http.h" #include "json.h" @@ -123,7 +124,7 @@ static int verify_checksums(const char *url, const char *path, const char *name) } /* case-insensitive compare */ if (strcasecmp(expect, hex) != 0) { - pmm_error("%s", pmm_tr_fmt("msg.checksum-mismatch", algos[i].ext, expect, hex)); + pmm_error_c(PMM_E_CHECKSUM, "文件校验和不匹配, 可能损坏, 重新下载或换镜像", "%s", pmm_tr_fmt("msg.checksum-mismatch", algos[i].ext, expect, hex)); return -1; } pmm_success("%s", pmm_tr_fmt("msg.checksum-ok", algos[i].algo == 256 ? "sha256" : "sha1", hex)); @@ -421,7 +422,7 @@ int install_file(const char *url, const char *name) { if (pmm_offline) { FILE *cf = fopen(path, "rb"); if (!cf) { - pmm_error("%s", pmm_tr_fmt("msg.err.not-cached", name)); + pmm_error_c(PMM_E_CACHE, "离线模式但本地没有该包缓存, 先联网 'pmm fetch <包名>'", "%s", pmm_tr_fmt("msg.err.not-cached", name)); return -1; } fclose(cf); @@ -445,7 +446,7 @@ int install_file(const char *url, const char *name) { free(cands); mirrors_free(ml); if (ok != 0) { - pmm_error("%s", pmm_tr_fmt("msg.err.download-failed", url)); + pmm_error_c(PMM_E_DOWNLOAD, "检查网络连接或镜像地址, 用 'pmm mirror check' 排障", "%s", pmm_tr_fmt("msg.err.download-failed", url)); return -1; } pmm_success("%s", pmm_tr_fmt("msg.downloaded", path)); @@ -457,7 +458,7 @@ int install_file(const char *url, const char *name) { if (pmm_sha1_file(path, hex) == 0) pmm_info("%s", pmm_tr_fmt("msg.hash-sha1", hex)); if (verify_checksums(url, path, name) != 0) { - pmm_error(pmm_tr("msg.err.checksum-refuse")); + pmm_error_c(PMM_E_CHECKSUM, "校验和不匹配, 拒绝安装, 尝试 'pmm fetch --fresh' 重新下载", "%s", pmm_tr("msg.err.checksum-refuse")); remove(path); return -1; } @@ -499,7 +500,7 @@ static int install_path(const char *path, const char *name) { extern int pdm_install_file(const char *path); pmm_info("%s", pmm_tr_fmt("msg.installing", bname)); if (pdm_install_file(path) != 0) { - pmm_error("%s", pmm_tr_fmt("msg.err.failed-install", bname)); + pmm_error_c(PMM_E_INTERNAL, ".pdm 安装失败, 详情见上方错误", "%s", pmm_tr_fmt("msg.err.failed-install", bname)); return -1; } pmm_success("%s", pmm_tr_fmt("msg.installed", bname)); @@ -571,7 +572,7 @@ static int install_path(const char *path, const char *name) { "elif command -v alien >/dev/null 2>&1; then sudo alien -i \"%s\"; " "else echo -e \"\\033[31m[PMM]:[ERROR]cannot unpack .rpm (need rpm/cpio)\\033[0m\" 1>&2; exit 1; fi", path, path, path); - if (system(cmd) != 0) { pmm_error(pmm_tr("msg.err.failed-install")); return -1; } + if (system(cmd) != 0) { pmm_error_c(PMM_E_INTERNAL, "rpm 安装失败, 检查是否缺少 rpm/rpm2cpio", "%s", pmm_tr("msg.err.failed-install")); return -1; } (void)dcmd; (void)ct; #else /* create the (possibly multi-level) stage dir first; pmm_cpio_unpack @@ -581,18 +582,18 @@ static int install_path(const char *path, const char *name) { char mkstag[1400]; snprintf(mkstag, sizeof(mkstag), "mkdir -p \"%s\"", fstage); if (system(mkstag) != 0) { - pmm_error("%s", pmm_tr_fmt("msg.err.rpm-stage", fstage)); + pmm_error_c(PMM_E_EXTRACT, "无法创建解包目录, 检查缓存目录权限", "%s", pmm_tr_fmt("msg.err.rpm-stage", fstage)); return -1; } } /* unpack into stage (no sudo needed; stage is user-writable) */ if (pmm_cpio_unpack_cmd(fstage, dcmd) != 0) { - pmm_error("%s", pmm_tr_fmt("msg.err.rpm-decompress", dc)); + pmm_error_c(PMM_E_EXTRACT, "rpm 解压失败, 可能需要 gzip/zstd 支持", "%s", pmm_tr_fmt("msg.err.rpm-decompress", dc)); return -1; } /* copy the unpacked payload into /, then clean up */ if (system(cmd) != 0) { - pmm_error(pmm_tr("msg.err.rpm-copy")); + pmm_error_c(PMM_E_INTERNAL, "rpm 拷贝到系统目录失败(可能权限不足, 需 sudo)", "%s", pmm_tr("msg.err.rpm-copy")); return -1; } (void)ct; @@ -656,7 +657,7 @@ static int install_path(const char *path, const char *name) { pmm_info("%s", pmm_tr_fmt("msg.installing", bname)); if (run_cmd_quiet(cmd) != 0) { - pmm_error("%s", pmm_tr_fmt("msg.err.failed-install", cmd)); + pmm_error_c(PMM_E_INTERNAL, "安装命令执行失败, 检查文件格式/权限(系统模式需 sudo)", "%s", pmm_tr_fmt("msg.err.failed-install", cmd)); return -1; } pmm_success("%s", pmm_tr_fmt("msg.installed", bname)); @@ -667,9 +668,9 @@ static int install_path(const char *path, const char *name) { /* Install a local file by its extension — e.g. `pmm install -dpkg foo.deb` * (Linux) or `pmm install -msi foo.msi` (Windows). Returns 0 on success. */ int install_local_file(const char *path) { - if (!path || !*path) { pmm_error(pmm_tr("msg.err.empty-path")); return -1; } + if (!path || !*path) { pmm_error_c(PMM_E_USAGE, "请提供要安装的文件路径", "%s", pmm_tr("msg.err.empty-path")); return -1; } FILE *chk = fopen(path, "rb"); - if (!chk) { pmm_error("%s", pmm_tr_fmt("msg.err.cannot-open", path)); return -1; } + if (!chk) { pmm_error_c(PMM_E_NOT_FOUND, "检查文件路径是否存在且可读", "%s", pmm_tr_fmt("msg.err.cannot-open", path)); return -1; } fclose(chk); pmm_info("%s", pmm_tr_fmt("msg.installing-file", path)); return install_path(path, path); @@ -818,7 +819,8 @@ int install_from_registry(const char *name, const char *spec, const char *mirror if (ml->items[i].registry && *ml->items[i].registry) has_reg = 1; if (!has_reg) { - pmm_error( + pmm_error_c(PMM_E_NO_MIRROR, + "配置镜像: 在 ~/.pmm/mirror.ini 添加 [名称] + registry 地址, 或用 'pmm setting mirror add'", "no registry mirror configured. Add to ~/.pmm/mirror.ini:\n" " [name]\n registry = https://host/pmm\n" " (apt-style: mirrors are tried by priority; the first with the\n" @@ -841,7 +843,7 @@ int install_from_registry(const char *name, const char *spec, const char *mirror if (!seen[i] && ml->items[i].registry && *ml->items[i].registry) bases[nb++] = ml->items[i].registry; - if (nb == 0) { pmm_error(pmm_tr("msg.err.no-registry-mirror")); mirrors_free(ml); return -1; } + if (nb == 0) { pmm_error_c(PMM_E_NO_MIRROR, "检查镜像配置, 添加可用镜像", "%s", pmm_tr("msg.err.no-registry-mirror")); mirrors_free(ml); return -1; } /* fetch the .json latest pointer (carries the variants list) */ char url[2048]; @@ -873,14 +875,14 @@ int install_from_registry(const char *name, const char *spec, const char *mirror if (used) break; } if (!body) { - pmm_error("%s", pmm_tr_fmt("msg.err.registry-not-found", name)); + pmm_error_c(PMM_E_NOT_FOUND, "检查包名拼写, 或 'pmm search' 搜索; 若确认存在, 可能镜像未同步", "%s", pmm_tr_fmt("msg.err.registry-not-found", name)); mirrors_free(ml); return -1; } JsonValue *meta = json_parse(body); free(body); if (!meta || meta->type != JSON_OBJECT) { - pmm_error("%s", pmm_tr_fmt("msg.err.bad-registry-entry", name, used_base)); + pmm_error_c(PMM_E_REGISTRY, "镜像条目损坏, 尝试刷新镜像或切换镜像源", "%s", pmm_tr_fmt("msg.err.bad-registry-entry", name, used_base)); json_free(meta); mirrors_free(ml); return -1; @@ -936,7 +938,7 @@ int install_from_registry(const char *name, const char *spec, const char *mirror const char *s = json_str(v, "sha256"); free(want_sha); want_sha = s ? strdup(s) : NULL; } } if (!chosen) { - pmm_error("%s", pmm_tr_fmt("msg.no-version", name, osn, arch, + pmm_error_c(PMM_E_NOT_FOUND, "当前系统/架构下没有匹配的版本, 或版本约束过于严格", "%s", pmm_tr_fmt("msg.no-version", name, osn, arch, (spec && *spec)) ? " satisfying '" : "", (spec && *spec) ? spec : ""); json_free(meta); mirrors_free(ml); return -1; } @@ -948,7 +950,7 @@ int install_from_registry(const char *name, const char *spec, const char *mirror pmm_info("%s", pmm_tr_fmt("msg.selected", name, json_str(meta, "version"), osn, "")); } if (!dl) { - pmm_error("%s", pmm_tr_fmt("msg.err.registry-entry-no-url", name)); + pmm_error_c(PMM_E_REGISTRY, "镜像中该条目缺少下载地址(url), 包可能未发布或损坏", "%s", pmm_tr_fmt("msg.err.registry-entry-no-url", name)); json_free(meta); mirrors_free(ml); return -1; } char *url_cp = strdup(dl); @@ -964,7 +966,7 @@ int install_from_registry(const char *name, const char *spec, const char *mirror snprintf(path, sizeof(path), "%s/%s", cache, file_cp); if (pmm_sha256_file(path, hex) == 0) { if (strcasecmp(want_sha, hex) != 0) { - pmm_error("%s", pmm_tr_fmt("msg.checksum-mismatch", name, want_sha, hex)); + pmm_error_c(PMM_E_CHECKSUM, "下载文件校验和不匹配, 可能被篡改或损坏, 重新下载", "%s", pmm_tr_fmt("msg.checksum-mismatch", name, want_sha, hex)); remove(path); rc = -1; } else { diff --git a/src/json.c b/src/json.c index 5cbd0b9..ad4c575 100644 --- a/src/json.c +++ b/src/json.c @@ -1,6 +1,7 @@ /* json.c - minimal recursive-descent JSON parser (no external deps) */ #include "json.h" #include "out.h" +#include "pmm_err.h" #include "i18n.h" #include #include @@ -13,7 +14,7 @@ static JsonValue *parse_value(Parser *ps); static JsonValue *jnew(JsonType t) { JsonValue *v = calloc(1, sizeof(JsonValue)); - if (!v) { pmm_error(pmm_tr("msg.err.oom")); exit(1); } + if (!v) { pmm_error_c(PMM_E_INTERNAL, "内存不足, 请释放一些资源后重试", "%s", pmm_tr("msg.err.oom")); exit(1); } v->type = t; return v; } @@ -37,7 +38,7 @@ static void jadd(JsonValue *v, char *key, JsonValue *item) { if (v->type == JSON_OBJECT) v->keys = realloc(v->keys, v->capacity * sizeof(char *)); if (!v->items || (v->type == JSON_OBJECT && !v->keys)) { - pmm_error(pmm_tr("msg.err.oom")); exit(1); + pmm_error_c(PMM_E_INTERNAL, "内存不足, 请释放一些资源后重试", "%s", pmm_tr("msg.err.oom")); exit(1); } } if (v->type == JSON_OBJECT) v->keys[v->count] = key; @@ -49,7 +50,7 @@ static void skip_ws(Parser *ps) { } static void fail(const char *msg) { - pmm_error("%s", pmm_tr_fmt("msg.err.json-parse", msg)); + pmm_error_c(PMM_E_INTERNAL, "JSON 解析失败, 数据可能损坏或被篡改", "%s", pmm_tr_fmt("msg.err.json-parse", msg)); exit(1); } diff --git a/src/main.c b/src/main.c index 6e50f18..6d354b2 100644 --- a/src/main.c +++ b/src/main.c @@ -30,6 +30,7 @@ #include "pmm.h" #include "out.h" +#include "pmm_err.h" #include "i18n.h" #include "json.h" #include "ini.h" @@ -223,13 +224,13 @@ static char *registry_fetch(const char *rel, int *outstatus) { /* pmm search — list registry packages matching keyword. */ static int cmd_search(int argc, char **argv) { - if (argc < 1) { pmm_error("%s", pmm_tr("msg.err.usage")); return 1; } + 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("%s", pmm_tr("msg.err.no-registry-index")); return 1; } + 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("%s", pmm_tr("msg.err.bad-registry-index")); json_free(root); return 1; } + 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++) { @@ -245,7 +246,7 @@ static int cmd_search(int argc, char **argv) { /* pmm info — registry package info, or a local .pdm's control. */ static int cmd_info(int argc, char **argv) { - if (argc < 1) { pmm_error("usage: pmm info \n"); return 1; } + if (argc < 1) { pmm_error_c(PMM_E_USAGE, "用法: pmm info <软件包名|文件.pdm>", "usage: pmm info \n"); return 1; } const char *pkg = argv[0]; /* local .pdm -> control info */ FILE *chk = fopen(pkg, "rb"); @@ -259,10 +260,10 @@ static int cmd_info(int argc, char **argv) { char rel[512]; snprintf(rel, sizeof(rel), "%s.json", pkg); int status = 0; char *body = registry_fetch(rel, &status); - if (!body) { pmm_error("%s", pmm_tr_fmt("msg.err.not-found", pkg)); return 1; } + 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("%s", pmm_tr_fmt("msg.err.bad-entry", pkg)); json_free(root); return 1; } + 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 : ""); @@ -443,7 +444,7 @@ static int cmd_upgrade(int argc, char **argv) { if (!(ans[0] == 'y' || ans[0] == 'Y')) continue; } if (install_from_registry(pkg, NULL, mirror.name) == 0) { upgraded++; } - else pmm_error("%s", pmm_tr_fmt("msg.err.failed-install", pkg)); + 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); @@ -546,10 +547,10 @@ static int save_registry_cache(const char *rel, const char *body) { static int cmd_update(void) { int status = 0; char *body = registry_fetch("packages.json", &status); - if (!body) { pmm_error("no registry index (packages.json) available\n"); return 1; } + 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("bad registry index\n"); if (root) json_free(root); free(body); return 1; + 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; } @@ -579,11 +580,11 @@ static int cmd_update(void) { /* pmm verify — print sha256 (and sha1) of a downloaded package file. */ static int cmd_verify(int argc, char **argv) { - if (argc < 1) { pmm_error("usage: pmm verify \n"); return 1; } + if (argc < 1) { pmm_error_c(PMM_E_USAGE, "用法: pmm verify <文件>", "usage: pmm verify \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("%s", pmm_tr_fmt("msg.err.cannot-read", file)); return 1; } + 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; } @@ -646,7 +647,7 @@ static int cmd_install_git(int argc, char **argv, const char *flag) { if (strcmp(argv[i], "--host") == 0 && i + 1 < argc) { host = host_from_name(argv[++i]); if (host == HOST_UNKNOWN) { - pmm_error("%s", pmm_tr_fmt("msg.err.unknown-host", argv[i])); + 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) { @@ -656,7 +657,8 @@ static int cmd_install_git(int argc, char **argv, const char *flag) { } } if (!repo) { - pmm_error( + pmm_error_c(PMM_E_USAGE, + "用法: pmm install --git / --github / --gitlab / --gitea ", "usage:\n" " pmm install --git any git host (auto-detected API)\n" " pmm install --github GitHub\n" @@ -680,7 +682,7 @@ static int cmd_install_git(int argc, char **argv, const char *flag) { "(tried gitea/gitlab/github shapes)\n", repo); if (!asset) { if (host != HOST_AUTO) - pmm_error("%s", pmm_tr_fmt("msg.err.no-suitable-asset", pmm_os_name(os), repo)); + 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; } @@ -736,7 +738,7 @@ static int cmd_mirror(int argc, char **argv) { * (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("%s", pmm_tr("msg.err.no-mirror")); ini_free(ini); return 1; } + if (!ini || !ini->head) { pmm_error_c(PMM_E_NO_MIRROR, "先添加镜像: 'pmm mirror add <名称> '", "%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; @@ -770,7 +772,7 @@ static int cmd_mirror(int argc, char **argv) { } if (strcmp(argv[0], "add") == 0 && argc >= 3) { FILE *f = fopen(path, "a"); - if (!f) { pmm_error("%s", pmm_tr_fmt("msg.err.cannot-write", path)); return 1; } + 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])); @@ -790,7 +792,7 @@ static int cmd_mirror(int argc, char **argv) { fclose(rf); } FILE *wf = fopen(cfgpath, "w"); - if (!wf) { pmm_error("%s", pmm_tr_fmt("msg.err.cannot-write", cfgpath)); return 1; } + 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]); @@ -801,9 +803,9 @@ static int cmd_mirror(int argc, char **argv) { } if (strcmp(argv[0], "remove") == 0 && argc >= 2) { Ini *ini = ini_load(path); - if (!ini) { pmm_error("%s", pmm_tr("msg.err.no-mirror-file")); return 1; } + 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("%s", pmm_tr_fmt("msg.err.cannot-write", path)); ini_free(ini); return 1; } + 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) { @@ -819,7 +821,7 @@ static int cmd_mirror(int argc, char **argv) { pmm_success("%s", pmm_tr_fmt("msg.mirror-removed", argv[1])); return 0; } - pmm_error("%s", pmm_tr("msg.err.usage")); + pmm_error_c(PMM_E_USAGE, "用法: pmm mirror ...", "%s", pmm_tr("msg.err.usage")); return 1; } @@ -923,7 +925,7 @@ static int cmd_cache(int argc, char **argv) { /* ---- pmm fetch ... : predownload packages into the cache (no install) ---- */ static int cmd_fetch(int argc, char **argv) { - if (argc < 1) { pmm_error("用法: pmm fetch ...\n"); return 1; } + if (argc < 1) { pmm_error_c(PMM_E_USAGE, "用法: pmm fetch <软件包名>...", "用法: pmm fetch ...\n"); return 1; } pmm_fetch_only = 1; int ok = 0; for (int i = 0; i < argc; i++) @@ -1151,7 +1153,7 @@ static void mkdir_p_local(char *path) { /* pmm setting lang ... */ static int cmd_setting(int argc, char **argv) { if (argc < 1) { - pmm_error("%s", pmm_tr("msg.err.usage")); + pmm_error_c(PMM_E_USAGE, "用法: pmm setting ...", "%s", pmm_tr("msg.err.usage")); return 1; } char dir[1024]; @@ -1171,9 +1173,9 @@ static int cmd_setting(int argc, char **argv) { /* 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; } + 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("%s", pmm_tr("msg.err.usage")); return 1; } + 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) { @@ -1185,7 +1187,7 @@ static int cmd_setting(int argc, char **argv) { fclose(rf); } FILE *wf = fopen(cfgpath, "w"); - if (!wf) { pmm_error("%s", pmm_tr_fmt("msg.err.cannot-write", cfgpath)); return 1; } + 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); @@ -1213,7 +1215,7 @@ static int cmd_setting(int argc, char **argv) { } if (strcmp(argv[0], "lang") != 0) { - pmm_error("%s", pmm_tr("msg.err.usage")); + pmm_error_c(PMM_E_USAGE, "可用子命令: get / set / mirror / lang", "%s", pmm_tr("msg.err.usage")); return 1; } char langdir[1200]; @@ -1244,9 +1246,9 @@ static int cmd_setting(int argc, char **argv) { } /* install / activate a specific locale: requires a value */ - if (argc < 2) { pmm_error("%s", pmm_tr("msg.err.usage")); return 1; } + 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("%s", pmm_tr_fmt("msg.err.invalid-locale", loc)); return 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 @@ -1277,7 +1279,7 @@ static int cmd_setting(int argc, char **argv) { if (http_download(url, out) == 0) { got = 1; break; } } if (!got) { - pmm_error("%s", pmm_tr_fmt("msg.err.download-failed", loc)); + 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); @@ -1293,7 +1295,7 @@ static int cmd_setting(int argc, char **argv) { fclose(rf); } FILE *wf = fopen(cfgpath, "w"); - if (!wf) { pmm_error("%s", pmm_tr_fmt("msg.err.cannot-write", cfgpath)); return 1; } + 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); @@ -1415,8 +1417,8 @@ int main(int argc, char **argv) { if (strcmp(argv[1], "setting") == 0) return cmd_setting(argc - 2, argv + 2); /* pmm remove (also used by the Windows "Uninstall" registration) */ if (strcmp(argv[1], "remove") == 0) { - if (argc < 3) { pmm_error("%s", pmm_tr("msg.err.usage")); return 1; } - if (argv[2][0] == '-') { pmm_error("%s", pmm_tr_fmt("msg.err.unknown-cmd", argv[2])); return 1; } + 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) */ @@ -1471,7 +1473,9 @@ int main(int argc, char **argv) { strcmp(argv[2], "--forgejo") == 0)) return cmd_install_git(argc - 3, argv + 3, argv[2] + 2); if (argc < 3) { - pmm_error("usage: pmm install [pkg2 ...]\n" + pmm_error_c(PMM_E_USAGE, + "用法: pmm install <软件包名|文件.deb|文件.msi|文件.rpm|文件.pdm> ...", + "usage: pmm install [pkg2 ...]\n" " | pmm install -dpkg install a .deb (Linux)\n" " | pmm install -rpm install an .rpm (any Linux, incl. Debian/RPM)\n" " | pmm install --git \n"); @@ -1501,7 +1505,7 @@ int main(int argc, char **argv) { /* 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("%s", pmm_tr_fmt("msg.err.unknown-cmd", argv[2])); + 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; } @@ -1532,7 +1536,7 @@ int main(int argc, char **argv) { } 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("%s", pmm_tr_fmt("msg.err.failed-install", pkg)); + 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 @@ -1544,14 +1548,14 @@ int main(int argc, char **argv) { else if (forced == 2) bn = "download.msi"; else if (forced == 3) bn = "download.rpm"; if (install_file(pkg, bn) == 0) ok++; - else pmm_error("%s", pmm_tr_fmt("msg.err.failed-install", pkg)); + 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("%s", pmm_tr_fmt("msg.err.failed-install", pkg)); + 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++; @@ -1563,7 +1567,7 @@ int main(int argc, char **argv) { if (strcmp(argv[1], "pack") == 0) { if (argc < 3) { - pmm_error("%s", pmm_tr("msg.err.usage")); + 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; @@ -1573,13 +1577,13 @@ int main(int argc, char **argv) { 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("%s", pmm_tr("msg.err.usage")); return 1; + pmm_error_c(PMM_E_USAGE, "用法: pmm cache ", "%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("%s", pmm_tr_fmt("msg.err.unknown-cmd", argv[1])); + pmm_error_c(PMM_E_USAGE, "未知子命令, 用 'pmm help' 查看可用命令", "%s", pmm_tr_fmt("msg.err.unknown-cmd", argv[1])); return 1; } diff --git a/src/out.c b/src/out.c index 0361d0c..9f90f35 100644 --- a/src/out.c +++ b/src/out.c @@ -1,5 +1,6 @@ /* out.c - colored/structured console output implementation */ #include "out.h" +#include "pmm_err.h" #include #include #include @@ -30,6 +31,11 @@ static void pmmsg(FILE *stream, int fd, const char *level, int wrap_color, const * this, "[PMM]:[INFO]msg\n" would print a blank line after every message. */ size_t len = strlen(buf); while (len && (buf[len-1] == '\n' || buf[len-1] == '\r')) buf[--len] = '\0'; + /* never emit an empty error/warn line */ + if (len == 0 && (strcmp(level, "ERROR") == 0 || strcmp(level, "WARN") == 0)) { + snprintf(buf, sizeof(buf), "(no message)"); + len = strlen(buf); + } int tty = IS_TTY_FD(fd) && !pmm_no_color; if (tty && wrap_color) @@ -44,6 +50,25 @@ void pmm_error(const char *fmt, ...) { va_end(ap); } +/* Structured error: [PMM]:[E]: (hint: ) + * Guarantees a non-empty message (falls back to "unknown error"). */ +void pmm_error_c(int code, const char *hint, const char *fmt, ...) { + char msg[4096]; + va_list ap; va_start(ap, fmt); + vsnprintf(msg, sizeof(msg), fmt ? fmt : "", ap); + va_end(ap); + size_t len = strlen(msg); + while (len && (msg[len-1] == '\n' || msg[len-1] == '\r')) msg[--len] = '\0'; + if (len == 0) snprintf(msg, sizeof(msg), "unknown error"); + if (hint && *hint) { + size_t ml = strlen(msg); + snprintf(msg + ml, sizeof(msg) - ml, " (hint: %s)", hint); + } + int tty = IS_TTY_FD(2) && !pmm_no_color; + if (tty) fprintf(stderr, "\033[31m[PMM]:[E%d]:%s\033[0m\n", code, msg); + else fprintf(stderr, "[PMM]:[E%d]:%s\n", code, msg); +} + 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); diff --git a/src/out.h b/src/out.h index f4e8f9c..1a87efc 100644 --- a/src/out.h +++ b/src/out.h @@ -26,6 +26,9 @@ extern int pmm_no_color; extern int pmm_log_level; void pmm_error(const char *fmt, ...); +/* Structured error: [PMM]:[E]: (hint: ). code is a PMM_E_*. + * Guarantees a non-empty message. */ +void pmm_error_c(int code, const char *hint, const char *fmt, ...); void pmm_success(const char *fmt, ...); void pmm_info(const char *fmt, ...); void pmm_warn(const char *fmt, ...); diff --git a/src/pdm.c b/src/pdm.c index c61d79f..f38d5c8 100644 --- a/src/pdm.c +++ b/src/pdm.c @@ -2,6 +2,7 @@ #include "pdm.h" #include "pmm.h" #include "out.h" +#include "pmm_err.h" #include "i18n.h" #include "install.h" #include "sha256.h" @@ -138,13 +139,13 @@ int pdm_pack(const char *dir, const char *out) { snprintf(cp_, sizeof(cp_), "%s/pdm-control", dir); char *control = read_file(cp_, NULL); if (!control) { - pmm_error("%s", pmm_tr_fmt("msg.err.no-control", dir)); + pmm_error_c(PMM_E_NOT_FOUND, "打包目录缺少 pdm-control 文件, 需先创建", "%s", pmm_tr_fmt("msg.err.no-control", dir)); return -1; } char *pkg = control_get(control, "Package"); char *ver = control_get(control, "Version"); if (!pkg || !*pkg) { - pmm_error(pmm_tr("msg.err.no-package")); + pmm_error_c(PMM_E_INTERNAL, "pdm-control 中缺少 Package 字段", "%s", pmm_tr("msg.err.no-package")); free(control); return -1; } if (!ver || !*ver) ver = strdup("0.0.0"); @@ -165,13 +166,13 @@ int pdm_pack(const char *dir, const char *out) { /* control.tar.gz (contains pdm-control; packed from the source dir) */ snprintf(cmd, sizeof(cmd), "tar -czf \"%s/control.tar.gz\" -C \"%s\" pdm-control", stage, dir); - if (system(cmd) != 0) { pmm_error(pmm_tr("msg.err.tar-control")); rmtree(stage); return -1; } + if (system(cmd) != 0) { pmm_error_c(PMM_E_EXTRACT, "检查系统是否安装 tar, 目录是否可读/写", "%s", pmm_tr("msg.err.tar-control")); rmtree(stage); return -1; } /* data.tar.gz (everything except pdm-control and the scratch dir) */ snprintf(cmd, sizeof(cmd), "tar -czf \"%s/data.tar.gz\" -C \"%s\" --exclude pdm-control --exclude %s .", stage, dir, stage); - if (system(cmd) != 0) { pmm_error(pmm_tr("msg.err.tar-data")); rmtree(stage); return -1; } + if (system(cmd) != 0) { pmm_error_c(PMM_E_EXTRACT, "检查系统是否安装 tar, 目录是否可读/写", "%s", pmm_tr("msg.err.tar-data")); rmtree(stage); return -1; } /* sha256sums of the two members */ char sums_path[1100], hex[128]; @@ -190,7 +191,7 @@ int pdm_pack(const char *dir, const char *out) { snprintf(cmd, sizeof(cmd), "tar -cf \"%s\" -C \"%s\" control.tar.gz data.tar.gz sha256sums", out, stage); int rc = system(cmd); rmtree(stage); - if (rc != 0) { pmm_error("%s", pmm_tr_fmt("msg.err.cannot-write", out)); return -1; } + if (rc != 0) { pmm_error_c(PMM_E_INTERNAL, "检查输出目录是否可写(系统模式需 sudo)", "%s", pmm_tr_fmt("msg.err.cannot-write", out)); return -1; } pmm_success("%s", pmm_tr_fmt("msg.packed", dir, pkg, ver, out)); free(control); free(pkg); @@ -458,7 +459,7 @@ int pdm_info(const char *pdmfile) { int pdm_install_file(const char *pdmfile) { if (!ends_with(pdmfile, ".pdm")) { - pmm_error("%s", pmm_tr_fmt("msg.err.not-pdm", pdmfile)); + pmm_error_c(PMM_E_USAGE, "请提供 .pdm 格式的安装包(如 <包名>_<版本>.pdm)", "%s", pmm_tr_fmt("msg.err.not-pdm", pdmfile)); return -1; } char home[1024], db[1024], root[1024], cmd[2600]; @@ -478,11 +479,11 @@ int pdm_install_file(const char *pdmfile) { char tmpname[1400], tmprel[] = "_pmm_install.pdm"; snprintf(tmpname, sizeof(tmpname), "%s/_pmm_install.pdm", home); if (copy_file(pdmfile, tmpname) != 0) { - pmm_error("%s", pmm_tr_fmt("msg.err.cannot-read", pdmfile)); + pmm_error_c(PMM_E_NOT_FOUND, "检查文件路径是否存在且可读", "%s", pmm_tr_fmt("msg.err.cannot-read", pdmfile)); return -1; } if (chdir_save(home) != 0) { remove(tmpname); return -1; } - if (system(NULL) == 0) { pmm_error(pmm_tr("msg.err.no-tar")); chdir_restore(); remove(tmpname); return -1; } + if (system(NULL) == 0) { pmm_error_c(PMM_E_NO_TAR, "系统缺少 tar, 请先安装 tar(如 apt install tar)", "%s", pmm_tr("msg.err.no-tar")); chdir_restore(); remove(tmpname); return -1; } const char *stage = "installed/.stage"; rmtree(stage); @@ -491,7 +492,7 @@ int pdm_install_file(const char *pdmfile) { /* extract members (all relative) */ snprintf(cmd, sizeof(cmd), "tar -xf \"%s\" -C \"%s\"", tmprel, stage); if (system(cmd) != 0) { - pmm_error("%s", pmm_tr_fmt("msg.err.bad-archive", pdmfile)); + pmm_error_c(PMM_E_EXTRACT, "该 .pdm 文件可能损坏或不是有效包, 重新下载", "%s", pmm_tr_fmt("msg.err.bad-archive", pdmfile)); chdir_restore(); remove(tmpname); return -1; } @@ -509,7 +510,7 @@ int pdm_install_file(const char *pdmfile) { char mp[1200]; snprintf(mp, sizeof(mp), "installed/.stage/%s", fname); if (pmm_sha256_file(mp, hex) != 0 || strcasecmp(hex, expect) != 0) { - pmm_error("%s", pmm_tr_fmt("msg.err.checksum-mismatch-file", fname, pdmfile)); + pmm_error_c(PMM_E_CHECKSUM, "文件校验和不匹配, 包可能损坏, 重新下载或换镜像", "%s", pmm_tr_fmt("msg.err.checksum-mismatch-file", fname, pdmfile)); fclose(sf); chdir_restore(); remove(tmpname); return -1; } } @@ -523,7 +524,7 @@ int pdm_install_file(const char *pdmfile) { snprintf(cmd, sizeof(cmd), "tar -xzOf \"%s/control.tar.gz\" pdm-control", stage); FILE *cf = PMM_POPEN_READ(cmd); if (!cf) { - pmm_error("%s", pmm_tr_fmt("msg.err.no-control-tar", pdmfile)); + pmm_error_c(PMM_E_EXTRACT, "包内缺少 control.tar.gz, 不是有效 .pdm", "%s", pmm_tr_fmt("msg.err.no-control-tar", pdmfile)); chdir_restore(); remove(tmpname); return -1; } char ctl[4096]; @@ -536,7 +537,7 @@ int pdm_install_file(const char *pdmfile) { char *pkg = control_get(ctl, "Package"); char *ver = control_get(ctl, "Version"); if (!pkg || !*pkg) { - pmm_error(pmm_tr("msg.err.no-package")); + pmm_error_c(PMM_E_INTERNAL, "包内 pdm-control 缺少 Package 字段, 不是有效 .pdm", "%s", pmm_tr("msg.err.no-package")); chdir_restore(); remove(tmpname); return -1; } @@ -560,7 +561,7 @@ int pdm_install_file(const char *pdmfile) { /* 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)); + pmm_error_c(PMM_E_CONFLICT, "先移除冲突包, 或使用 --force 覆盖安装", "%s", pmm_tr_fmt("msg.err.conflict", pkg, confl)); free(confl); free(pkg); if (ver) free(ver); chdir_restore(); remove(tmpname); return -1; } @@ -582,7 +583,7 @@ int pdm_install_file(const char *pdmfile) { int self_moved = move_self_aside(self_target); snprintf(cmd, sizeof(cmd), "tar -xzf \"%s/data.tar.gz\" -C \"%s\"", stage, tgt); if (system(cmd) != 0) { - pmm_error(pmm_tr("msg.err.extract")); + pmm_error_c(PMM_E_EXTRACT, "解包失败, 下载文件可能损坏, 重新下载或换镜像", "%s", pmm_tr("msg.err.extract")); if (self_moved) restore_aside(self_target); /* put old binary back */ chdir_restore(); remove(tmpname); return -1; } @@ -767,7 +768,7 @@ int pdm_remove(const char *name) { long len = 0; char *info = read_file(ipath, &len); if (!info) { - pmm_error("%s", pmm_tr_fmt("msg.err.not-installed", name)); + pmm_error_c(PMM_E_NOT_FOUND, "该包未安装, 用 'pmm list' 查看已安装的包", "%s", pmm_tr_fmt("msg.err.not-installed", name)); return -1; } char *files = strstr(info, "Files:\n"); diff --git a/src/pmm_err.h b/src/pmm_err.h new file mode 100644 index 0000000..83f34ac --- /dev/null +++ b/src/pmm_err.h @@ -0,0 +1,26 @@ +/* pmm_err.h — stable PMM error codes. + * + * Every user-facing error carries one of these codes so scripts/tools and users + * can reliably identify it. Pair it with a human hint via pmm_error_c(). */ +#ifndef PMM_ERR_H +#define PMM_ERR_H + +enum { + PMM_E_OK = 0, /* 成功 */ + PMM_E_USAGE = 1, /* 用法/参数错误 */ + PMM_E_NOT_FOUND = 2, /* 找不到软件包/文件 */ + PMM_E_DOWNLOAD = 3, /* 下载失败 */ + PMM_E_NETWORK = 4, /* 网络/连接问题 */ + PMM_E_NO_MIRROR = 5, /* 未配置/无可用镜像 */ + PMM_E_REGISTRY = 6, /* 注册表条目异常 */ + PMM_E_CHECKSUM = 7, /* 校验和不匹配/缺失 */ + PMM_E_EXTRACT = 8, /* 解包/解压失败 */ + PMM_E_CONFLICT = 9, /* 依赖/冲突 */ + PMM_E_NO_TAR = 10, /* 缺少 tar */ + PMM_E_NO_ROOT = 11, /* 需要 root/权限不足 */ + PMM_E_CACHE = 12, /* 缓存问题(未缓存/写入) */ + PMM_E_LANG = 13, /* 语言包错误 */ + PMM_E_INTERNAL = 99 /* 内部/未知错误 */ +}; + +#endif diff --git a/src/ppdm.c b/src/ppdm.c index 6c4292c..e68c2a9 100644 --- a/src/ppdm.c +++ b/src/ppdm.c @@ -11,6 +11,7 @@ * Config is stored in ~/.ppdm/config (Windows: D:\.ppdm\config). */ #include "out.h" +#include "pmm_err.h" #include "sha256.h" #include "pdm.h" #include "http.h" @@ -83,7 +84,7 @@ static void cfg_save_token(const char *token, const char *email) { snprintf(g_token, sizeof(g_token), "%s", token ? token : ""); snprintf(g_email, sizeof(g_email), "%s", email ? email : ""); FILE *f = fopen(g_cfg, "w"); - if (!f) { pmm_error("无法写入 %s\n", g_cfg); return; } + if (!f) { pmm_error_c(PMM_E_INTERNAL, "检查配置目录是否可写", "无法写入 %s\n", g_cfg); return; } fprintf(f, "server = %s\n", g_server); fprintf(f, "token = %s\n", g_token); fprintf(f, "email = %s\n", g_email); @@ -99,7 +100,7 @@ static int http_post(const char *url, const char *auth, const char *body, auth ? auth : "", file, url); } else { i += snprintf(cmd + i, sizeof(cmd) - (size_t)i, "curl -s -X POST %s -H 'Content-Type: application/json' --data-binary '%s' '%s'", auth ? auth : "", body ? body : "", url); } - if (i < 0 || (size_t)i >= sizeof(cmd)) { pmm_error("请求过长\n"); return -1; } + if (i < 0 || (size_t)i >= sizeof(cmd)) { pmm_error_c(PMM_E_INTERNAL, "请求内容过长, 请简化输入", "请求过长\n"); return -1; } FILE *f = popen(cmd, "r"); if (!f) return -1; size_t got = fread(out, 1, outs - 1, f); @@ -148,9 +149,9 @@ static void js_err_print(const char *body) { if (q && ((size_t)(q - e)) < n) n = (size_t)(q - e); if (n >= sizeof(buf)) n = sizeof(buf) - 1; memcpy(buf, e, n); buf[n] = 0; - pmm_error("%s\n", buf); + pmm_error_c(PMM_E_INTERNAL, "服务器返回了错误信息, 请检查账号或服务状态", "%s\n", buf); } else { - pmm_error("%s\n", body ? body : "(no response)"); + pmm_error_c(PMM_E_NETWORK, "服务器无响应, 检查网络或服务是否在线", "%s\n", body ? body : "(no response)"); } } @@ -169,7 +170,7 @@ static int captcha(void) { fflush(stdout); char in[64]; if (!fgets(in, sizeof(in), stdin)) return 0; - if (atoi(in) != ans) { pmm_error("验证错误\n"); return 0; } + if (atoi(in) != ans) { pmm_error_c(PMM_E_USAGE, "验证码不对, 请重新输入正确的计算结果", "验证错误\n"); return 0; } pmm_success("人机验证通过\n"); return 1; } @@ -252,9 +253,9 @@ int main(int argc, char **argv) { /* ---- register ---- */ if (strcmp(argv[1], "register") == 0) { - if (argc < 4) { pmm_error("用法: ppdm register \n"); return 1; } + if (argc < 4) { pmm_error_c(PMM_E_USAGE, "用法: ppdm register <邮箱> <密码>", "用法: ppdm register \n"); return 1; } const char *email = argv[2], *pass = argv[3]; - if (strchr(email, '\'') || strchr(pass, '\'')) { pmm_error("邮箱/密码不能含单引号\n"); return 1; } + if (strchr(email, '\'') || strchr(pass, '\'')) { pmm_error_c(PMM_E_USAGE, "邮箱/密码不能包含单引号", "邮箱/密码不能含单引号\n"); return 1; } if (!captcha()) return 1; snprintf(url, sizeof(url), "%s/register.php", g_server); char json[1024]; @@ -267,9 +268,9 @@ int main(int argc, char **argv) { /* ---- login ---- */ if (strcmp(argv[1], "login") == 0) { - if (argc < 4) { pmm_error("用法: ppdm login \n"); return 1; } + if (argc < 4) { pmm_error_c(PMM_E_USAGE, "用法: ppdm login <邮箱> <密码>", "用法: ppdm login \n"); return 1; } const char *email = argv[2], *pass = argv[3]; - if (strchr(email, '\'') || strchr(pass, '\'')) { pmm_error("邮箱/密码不能含单引号\n"); return 1; } + if (strchr(email, '\'') || strchr(pass, '\'')) { pmm_error_c(PMM_E_USAGE, "邮箱/密码不能包含单引号", "邮箱/密码不能含单引号\n"); return 1; } snprintf(url, sizeof(url), "%s/login.php", g_server); char json[1024]; snprintf(json, sizeof(json), "{\"email\":\"%s\",\"password\":\"%s\"}", email, pass); @@ -305,7 +306,7 @@ int main(int argc, char **argv) { /* ---- pack [out] : build a .pdm from a staging dir (pdm-control required) ---- */ if (strcmp(argv[1], "pack") == 0) { - if (argc < 3) { pmm_error("用法: ppdm pack [out]\n"); return 1; } + if (argc < 3) { pmm_error_c(PMM_E_USAGE, "用法: ppdm pack <目录> [输出.pdm]", "用法: ppdm pack [out]\n"); return 1; } return pdm_pack(argv[2], argc >= 4 ? argv[3] : NULL) == 0 ? 0 : 1; } @@ -319,11 +320,11 @@ int main(int argc, char **argv) { PPDM_MKDIR(bdir); pmm_info("正在下载最新 ppdm ...\n"); if (http_download("https://github.com/JGZYES/ParlzPackageManger/releases/latest/download/ppdm", tmp) != 0) { - pmm_error("更新下载失败\n"); return 1; + pmm_error_c(PMM_E_DOWNLOAD, "检查网络或稍后重试, 也可手动从 release 下载 ppdm", "更新下载失败\n"); return 1; } chmod(tmp, 0755); /* ignore result; run-permission is fine on POSIX */ remove(bin); - if (rename(tmp, bin) != 0) { pmm_error("无法替换 %s\n", bin); return 1; } + if (rename(tmp, bin) != 0) { pmm_error_c(PMM_E_INTERNAL, "检查 ~/.ppdm/bin 是否可写", "无法替换 %s\n", bin); return 1; } pmm_success("已更新到 %s(把 %s 加入 PATH,下次运行即新版)\n", bin, bdir); return 0; } @@ -333,12 +334,12 @@ int main(int argc, char **argv) { if (strcmp(argv[1], "publish") == 0) file = argc > 2 ? argv[2] : NULL; else if (strlen(argv[1]) > 4 && strcmp(argv[1] + strlen(argv[1]) - 4, ".pdm") == 0) file = argv[1]; if (file) { - if (!g_token[0]) { pmm_error("请先 ppdm login\n"); return 1; } + if (!g_token[0]) { pmm_error_c(PMM_E_USAGE, "请先执行 ppdm login", "请先 ppdm login\n"); return 1; } char pkg[256], ver[128], arch[64]; pdm_meta(file, pkg, sizeof(pkg), ver, sizeof(ver), arch, sizeof(arch)); - if (!pkg[0] || !ver[0]) { pmm_error("无法解析 %s 的 Package/Version\n", file); return 1; } + if (!pkg[0] || !ver[0]) { pmm_error_c(PMM_E_INTERNAL, "该 .pdm 缺少 Package/Version, 请用 ppdm pack 正确打包", "无法解析 %s 的 Package/Version\n", file); return 1; } char hex[128]; - if (pmm_sha256_file(file, hex) != 0) { pmm_error("无法计算 sha256\n"); return 1; } + if (pmm_sha256_file(file, hex) != 0) { pmm_error_c(PMM_E_INTERNAL, "无法读取该文件, 检查权限", "无法计算 sha256\n"); return 1; } /* URL-encode description (name/version/arch/os are safe charsets) */ char desc[512]; snprintf(desc, sizeof(desc), "%s %s", pkg, ver); @@ -364,7 +365,7 @@ int main(int argc, char **argv) { g_server, pkg, ver, cpuarch[0] ? cpuarch : "amd64", os, sp); snprintf(auth, sizeof(auth), "-H 'Authorization: Bearer %s'", g_token); pmm_info("正在发布 %s %s ...\n", pkg, ver); - if (http_post(url, auth, NULL, file, resp, sizeof(resp)) <= 0) { pmm_error("发布失败(无响应)\n"); return 1; } + if (http_post(url, auth, NULL, file, resp, sizeof(resp)) <= 0) { pmm_error_c(PMM_E_NETWORK, "服务器无响应, 检查网络或服务是否在线", "发布失败(无响应)\n"); return 1; } if (js_ok(resp)) { char u[2200] = ""; js_val(resp, "url", u, sizeof(u)); pmm_success("发布成功: %s\n", u[0] ? u : file); @@ -372,6 +373,6 @@ int main(int argc, char **argv) { return js_ok(resp) ? 0 : 1; } - pmm_error("未知命令: %s (试试 ppdm help)\n", argv[1]); + pmm_error_c(PMM_E_USAGE, "未知子命令, 用 'ppdm help' 查看", "未知命令: %s (试试 ppdm help)\n", argv[1]); return 1; }