文件
ParlzPackageManger/src/pmm.c
T
JGZYES 1b152e950b fix: pmm self install/upgrade now takes effect immediately
- pdm_install_file: when the 'pmm' package is installed, copy the freshly
  built binary over the currently-running pmm path (e.g. /usr/local/bin/pmm)
  and clear the shell command hash, so 'pmm -v' reflects the new version
  without a manual 'hash -r'. Falls back to a hint when the target dir is
  not writable (non-root).
- pmm_add_to_path: prepend PMM dirs on Windows instead of appending, so
  ~/.pmm/root takes priority over /usr/local/bin (matches Linux .bashrc).
- release.yml: add -D_GNU_SOURCE so newer gcc exposes strdup/popen/realpath
  (fixes implicit-declaration build errors).
- CHANGELOG/0.3.7.md
2026-09-05 14:39:29 +08:00

542 行
19 KiB
C

/* pmm.c - platform & config-dir helpers */
#include "pmm.h"
#include "out.h"
#include "i18n.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <sys/stat.h>
#ifdef _WIN32
#include <direct.h>
#include <io.h>
#include <inttypes.h>
#define PMM_MKDIR(p) _mkdir(p)
#define PMM_POPEN_READ(cmd) _popen(cmd, "r")
#define PMM_PCLOSE_READ(p) _pclose(p)
#else
#include <sys/types.h>
#include <dirent.h>
#include <strings.h> /* strcasecmp/strncasecmp */
#define PMM_MKDIR(p) mkdir((p), 0755)
#define PMM_POPEN_READ(cmd) popen(cmd, "r")
#define PMM_PCLOSE_READ(p) pclose(p)
#endif
static char g_self_path[4096] = ""; /* >= PATH_MAX: realpath FORTIFY requires it */
void pmm_set_self_path(const char *argv0) {
if (!argv0 || !*argv0) return;
#ifdef _WIN32
if (_fullpath(g_self_path, argv0, sizeof(g_self_path)) == NULL)
snprintf(g_self_path, sizeof(g_self_path), "%s", argv0);
#else
/* realpath(_, NULL) allocates internally — no fixed-buffer overflow, and
* no FORTIFY __realpath_chk abort when the caller buffer is < PATH_MAX. */
char *rp = realpath(argv0, NULL);
if (rp) {
snprintf(g_self_path, sizeof(g_self_path), "%s", rp);
free(rp);
} else {
snprintf(g_self_path, sizeof(g_self_path), "%s", argv0);
}
#endif
}
const char *pmm_self_path(void) {
return g_self_path[0] ? g_self_path : NULL;
}
PmmOS pmm_detect_os(void) {
#if defined(_WIN32)
return OS_WINDOWS;
#elif defined(__APPLE__)
return OS_MACOS;
#elif defined(__linux__)
return OS_LINUX;
#else
return OS_UNKNOWN;
#endif
}
const char *pmm_os_name(PmmOS os) {
switch (os) {
case OS_WINDOWS: return "windows";
case OS_LINUX: return "linux";
case OS_MACOS: return "macos";
default: return "unknown";
}
}
const char *pmm_detect_arch(void) {
#if defined(_WIN32)
/* Windows: x64 vs arm64 (ARM64 / AMD64 env) */
const char *p = getenv("PROCESSOR_ARCHITECTURE");
if (p && (strcmp(p, "ARM64") == 0 || strcmp(p, "arm64") == 0)) return "aarch64";
const char *w = getenv("PROCESSOR_ARCHITEW6432");
if (w && (strcmp(w, "ARM64") == 0)) return "aarch64";
return "amd64";
#elif defined(__aarch64__) || defined(__arm64__)
return "aarch64";
#elif defined(__x86_64__) || defined(_M_X64)
return "amd64";
#elif defined(__i386__) || defined(__i686__) || defined(_M_IX86)
return "x86";
#else
return "any";
#endif
}
static void mkdir_p(const char *path) {
char tmp[1024];
strncpy(tmp, path, sizeof(tmp) - 1);
tmp[sizeof(tmp) - 1] = '\0';
for (char *p = tmp + 1; *p; p++) {
if (*p == '/' || *p == '\\') {
char c = *p;
*p = '\0';
PMM_MKDIR(tmp);
*p = c;
}
}
PMM_MKDIR(tmp);
}
static const char *home_dir(void) {
const char *h = getenv("USERPROFILE"); /* windows */
if (!h || !*h) h = getenv("HOME");
return (h && *h) ? h : ".";
}
/* ---- install-location overrides ----
* Either a drive letter ("D" -> D:\.pmm) via -p<drive>, or an exact path
* ("D:\apps\pmm" / "/opt/pmm") via -p <path>. The path override wins. */
static char g_drive[4] = ""; /* "D", or "" = use home */
static char g_base_path[1024] = ""; /* exact install base, or "" */
static const char *drive_state_path(char *buf, size_t size) {
snprintf(buf, size, "%s/.pmm/install-drive", home_dir());
return buf;
}
void pmm_set_install_drive(const char *letter) {
if (!letter || !*letter) { g_drive[0] = '\0'; return; }
char c = (char)((*letter >= 'a' && *letter <= 'z') ? *letter - 'a' + 'A' : *letter);
if (c < 'A' || c > 'Z') return; /* only a single A-Z drive letter */
g_drive[0] = c;
g_drive[1] = '\0';
char state[1024];
drive_state_path(state, sizeof(state));
char dir[1024];
snprintf(dir, sizeof(dir), "%s/.pmm", home_dir());
mkdir_p(dir);
FILE *f = fopen(state, "w");
if (f) { fprintf(f, "%c", c); fclose(f); }
pmm_success("%s", pmm_tr_fmt("msg.drive-set", c));
}
void pmm_set_install_path(const char *path) {
if (!path || !*path) { g_base_path[0] = '\0'; return; }
snprintf(g_base_path, sizeof(g_base_path), "%s", path);
/* strip trailing slashes */
size_t l = strlen(g_base_path);
while (l > 1 && (g_base_path[l-1] == '/' || g_base_path[l-1] == '\\')) g_base_path[--l] = '\0';
mkdir_p(g_base_path);
pmm_success("%s", pmm_tr_fmt("msg.path-set", g_base_path));
}
int pmm_flat_mode(void) { return g_base_path[0] ? 1 : 0; }
/* Resolve the .pmm base dir: explicit drive override > persisted drive >
* home/.pmm (when that drive is picked) > DEFAULT = D:\.pmm.
* Defaulting to D:\ spares the C: drive (user's primary diskspace concern);
* use -pc to go back to the user-home .pmm, or -p<drive> for any other drive. */
static char home_drive_letter(void) {
const char *h = home_dir();
if (h[0] && h[1] == ':' && ((h[0] >= 'A' && h[0] <= 'Z') || (h[0] >= 'a' && h[0] <= 'z')))
return (char)((h[0] >= 'a' && h[0] <= 'z') ? h[0] - 'a' + 'A' : h[0]);
return 0; /* not a drive-letter home (unix) */
}
static const char *pmm_base_dir(char *buf, size_t size) {
if (g_base_path[0]) { /* -p <exact path> */
snprintf(buf, size, "%s", g_base_path);
return buf;
}
char drive[4] = "";
if (g_drive[0]) {
drive[0] = g_drive[0];
} else {
char state[1024];
drive_state_path(state, sizeof(state));
FILE *f = fopen(state, "r");
if (f) {
char c = (char)fgetc(f);
fclose(f);
if (c >= 'A' && c <= 'Z') drive[0] = c;
else if (c >= 'a' && c <= 'z') drive[0] = (char)(c - 'a' + 'A');
}
}
char hd = home_drive_letter();
if (drive[0] && hd && drive[0] != hd) {
snprintf(buf, size, "%c:\\.pmm", drive[0]); /* -p<other> */
} else if (drive[0] && hd && drive[0] == hd) {
snprintf(buf, size, "%s/.pmm", home_dir()); /* -pc -> home */
} else {
#ifdef _WIN32
snprintf(buf, size, "D:\\.pmm"); /* Windows DEFAULT -> D: */
#else
snprintf(buf, size, "%s/.pmm", home_dir()); /* unix default -> ~/.pmm */
#endif
}
return buf;
}
const char *pmm_config_dir(char *buf, size_t size) {
pmm_base_dir(buf, size);
mkdir_p(buf);
return buf;
}
const char *pmm_cache_dir(char *buf, size_t size) {
char base[1024];
pmm_base_dir(base, sizeof(base));
snprintf(buf, size, "%s/cache", base);
mkdir_p(buf);
return buf;
}
const char *pmm_install_dir(char *buf, size_t size) {
const char *env = getenv("PMM_INSTALL_DIR");
if (env && *env) { snprintf(buf, size, "%s", env); }
else if (g_base_path[0]) {
snprintf(buf, size, "%s", g_base_path); /* -p <path>: flat install into the specified dir */
} else {
#ifdef _WIN32
char base[1024];
pmm_base_dir(base, sizeof(base));
snprintf(buf, size, "%s/bin", base); /* Windows default -> <base>\bin */
#else
snprintf(buf, size, "/usr/local/bin"); /* Linux/macOS default -> system-wide (on PATH) */
#endif
}
mkdir_p(buf);
return buf;
}
char *pmm_find_config(const char *dir, const char *base) {
static const char *exts[] = { ".json", ".ini", ".conf", NULL };
static char out[1024];
for (int i = 0; exts[i]; i++) {
snprintf(out, sizeof(out), "%s/%s%s", dir, base, exts[i]);
struct stat st;
if (stat(out, &st) == 0 && !S_ISDIR(st.st_mode))
return out;
}
return NULL;
}
/* ---------- PATH auto-add ---------- */
static int dir_has_exec(const char *dir) {
#ifdef _WIN32
char patt[1300];
snprintf(patt, sizeof(patt), "%s\\*", dir);
struct _finddata_t fd;
intptr_t h = _findfirst(patt, &fd);
if (h == -1) return 0;
int found = 0;
do {
if (fd.attrib & _A_SUBDIR) continue;
size_t l = strlen(fd.name);
if ((l > 4 && strcasecmp(fd.name + l - 4, ".exe") == 0) ||
(l > 4 && strcasecmp(fd.name + l - 4, ".cmd") == 0) ||
(l > 4 && strcasecmp(fd.name + l - 4, ".bat") == 0)) { found = 1; break; }
} while (_findnext(h, &fd) == 0);
_findclose(h);
return found;
#else
DIR *d = opendir(dir);
if (!d) return 0;
struct dirent *e;
int found = 0;
while ((e = readdir(d)) != NULL) {
if (e->d_name[0] == '.') continue;
char full[1300];
snprintf(full, sizeof(full), "%s/%s", dir, e->d_name);
struct stat st;
if (stat(full, &st) == 0 && !S_ISDIR(st.st_mode)) { found = 1; break; }
}
closedir(d);
return found;
#endif
}
/* case-insensitive "dir" present in a ';'-separated PATH (win) / ':' (unix) */
static int path_has(const char *path, const char *dir, char sep) {
if (!path || !*path) return 0;
size_t dl = strlen(dir);
const char *p = path;
for (;;) {
const char *e = strchr(p, sep);
size_t n = e ? (size_t)(e - p) : strlen(p);
while (n && (p[n-1]==' '||p[n-1]=='"')) n--;
const char *q = p;
while (*q==' '||*q=='"') q++;
if (n && strncasecmp(q, dir, dl) == 0 && (n == dl)) return 1;
if (!e) break;
p = e + 1;
}
return 0;
}
/* Add a dir to the candidate list if not already listed. */
static void add_path_candidate(char list[][1200], int *n, int max, const char *dir) {
for (int i = 0; i < *n; i++)
if (strcmp(list[i], dir) == 0) return;
if (*n < max) snprintf(list[(*n)++], 1200, "%s", dir);
}
void pmm_add_to_path(void) {
char home[1024];
pmm_config_dir(home, sizeof(home));
char base[1024];
if (pmm_flat_mode()) snprintf(base, sizeof(base), "%s", home); /* -p <path> */
else snprintf(base, sizeof(base), "%s/root", home);
/* collect dirs: ~/.pmm/bin, ~/.pmm/root, and root subdirs holding executables */
char list[160][1200];
int n = 0;
char bin[1100];
snprintf(bin, sizeof(bin), "%s/bin", home);
mkdir_p(bin);
snprintf(list[n++], sizeof(list[0]), "%s", bin);
snprintf(list[n++], sizeof(list[0]), "%s", base);
mkdir_p(base);
#ifdef _WIN32
{
char patt[1600];
snprintf(patt, sizeof(patt), "%s\\*", base);
char cmd[1700];
snprintf(cmd, sizeof(cmd), "dir /b /ad \"%s\" 2>nul", patt);
FILE *f = PMM_POPEN_READ(cmd);
if (f) {
char sub[1200];
while (fgets(sub, sizeof(sub), f)) {
size_t l = strlen(sub);
while (l && (sub[l-1]=='\r'||sub[l-1]=='\n')) sub[--l] = 0;
if (!l) continue;
char full[1300];
snprintf(full, sizeof(full), "%s/%s", base, sub);
if (dir_has_exec(full)) {
add_path_candidate(list, &n, 160, full);
} else {
/* depth-2: pkg/cmd convention (e.g. git/cmd/git.exe) */
char cmddir[1400];
snprintf(cmddir, sizeof(cmddir), "%s/cmd", full);
if (dir_has_exec(cmddir))
add_path_candidate(list, &n, 160, cmddir);
}
}
PMM_PCLOSE_READ(f);
}
}
#else
{
char cmd[1700];
snprintf(cmd, sizeof(cmd), "ls -1 \"%s\" 2>/dev/null", base);
FILE *f = PMM_POPEN_READ(cmd);
if (f) {
char sub[1200];
while (fgets(sub, sizeof(sub), f)) {
size_t l = strlen(sub);
while (l && (sub[l-1]=='\r'||sub[l-1]=='\n')) sub[--l] = 0;
char full[1300];
snprintf(full, sizeof(full), "%s/%s", base, sub);
if (dir_has_exec(full)) {
add_path_candidate(list, &n, 160, full);
} else {
char cmddir[1400];
snprintf(cmddir, sizeof(cmddir), "%s/cmd", full);
if (dir_has_exec(cmddir))
add_path_candidate(list, &n, 160, cmddir);
}
}
PMM_PCLOSE_READ(f);
}
}
#endif
/* read current user PATH */
char cur[16384] = "";
#ifdef _WIN32
{
FILE *f = PMM_POPEN_READ("reg query \"HKCU\\Environment\" /v Path 2>nul");
if (f) {
char line[8192];
char *val = NULL;
while (fgets(line, sizeof(line), f)) {
char *r = strstr(line, "REG_SZ");
if (r) { val = r + 6; while (*val==' '||*val=='\t'||*val=='\r'||*val=='\n') val++; break; }
}
PMM_PCLOSE_READ(f);
if (val) {
size_t v = strlen(val);
while (v && (val[v-1]=='\r'||val[v-1]=='\n')) val[--v] = 0;
snprintf(cur, sizeof(cur), "%s", val);
}
}
}
#else
{
const char *env = getenv("PATH");
if (env) snprintf(cur, sizeof(cur), "%s", env);
}
#endif
/* prepend any missing PMM dirs so they take priority over /usr/local/bin
* (an apt/system install could otherwise shadow a pmm-managed binary). */
char prefix[17000] = "";
size_t plen = 0;
int added = 0;
for (int i = 0; i < n; i++) {
if (!list[i][0]) continue;
if (path_has(cur, list[i], ';') || path_has(cur, list[i], ':')
|| path_has(prefix, list[i], ';') || path_has(prefix, list[i], ':')) continue;
if (plen) { if (plen + 1 < sizeof(prefix)) prefix[plen++] = ';'; }
size_t room = sizeof(prefix) - plen - 1;
if (room <= 0) break; /* no space left: stop */
snprintf(prefix + plen, room + 1, "%s", list[i]);
plen += strlen(list[i]);
added++;
pmm_info("%s", pmm_tr_fmt("msg.path-adding", list[i]));
}
if (!added) { pmm_info(pmm_tr("msg.path-exists")); return; }
/* final value: PMM dirs (prefix) then the existing startup PATH (cur).
* Only Windows persists it (setx); Linux prepends via .bashrc below. */
char next[17000] = "";
snprintf(next, sizeof(next), "%s;%s", prefix, cur);
#ifdef _WIN32
{
char cmd[17001];
/* drive letters and ';' are fine for setx; quote the value */
snprintf(cmd, sizeof(cmd), "setx Path \"%s\" >nul 2>&1", next);
system(cmd);
pmm_success(pmm_tr("msg.path-updated"));
}
#else
{
const char *she = getenv("SHELL");
const char *rc = "~/.bashrc";
char rcp[1100];
snprintf(rcp, sizeof(rcp), "%s/.bashrc", home_dir());
FILE *f = fopen(rcp, "a");
if (f) {
fprintf(f, "\n# PMM PATH\n");
for (int i = 0; i < n; i++) if (list[i][0]) fprintf(f, "export PATH=\"%s:$PATH\"\n", list[i]);
fclose(f);
}
(void)she;
pmm_success("%s", pmm_tr_fmt("msg.path-reload", rcp));
}
#endif
}
/* ---------- register installed packages in the Windows "apps" (Uninstall) ----------
* Mimics what an MSI installer writes: HKCU\Software\Microsoft\
* Windows\CurrentVersion\Uninstall\<pkg>, so the package appears in
* "Installed apps" / Add-Remove Programs and can be uninstalled via pmm. */
/* Escape a string for a single-quoted PowerShell literal (backslashes are
* literal there; only a single quote needs doubling: ' -> ''). */
static void ps_squote(const char *in, char *out, size_t outsz) {
size_t o = 0;
for (const char *p = in; *p && o + 3 < outsz; p++) {
if (*p == '\'') { out[o++] = '\''; out[o++] = '\''; }
out[o++] = *p;
}
out[o] = '\0';
}
int pmm_reg_uninstall(const char *pkg, const char *ver, const char *pub,
const char *install_location, const char *icon,
const char *uninstaller, unsigned long long size_bytes) {
#ifndef _WIN32
(void)pkg; (void)ver; (void)pub; (void)install_location; (void)icon;
(void)uninstaller; (void)size_bytes;
return 0; /* only Windows registers software in the Uninstall hive */
#else
if (!pkg || !*pkg) return -1;
char base[1024];
pmm_config_dir(base, sizeof(base));
char cfg[1100];
snprintf(cfg, sizeof(cfg), "%s\\cache", base);
mkdir_p(base); mkdir_p(cfg);
char ps1[1300];
snprintf(ps1, sizeof(ps1), "%s\\pmm-uninst-%s.ps1", cfg, pkg);
/* PowerShell-escape the values */
char e_nm[700], e_ver[700], e_pub[700], e_loc[2400], e_icon[2600], e_un[4200];
ps_squote(pkg, e_nm, sizeof(e_nm));
ps_squote(ver ? ver : "", e_ver, sizeof(e_ver));
ps_squote(pub ? pub : "", e_pub, sizeof(e_pub));
ps_squote(install_location ? install_location : "", e_loc, sizeof(e_loc));
ps_squote(icon ? icon : "", e_icon, sizeof(e_icon));
char uninst[2048];
if (uninstaller && *uninstaller) snprintf(uninst, sizeof(uninst), "\"%s\" remove %s", uninstaller, pkg);
else snprintf(uninst, sizeof(uninst), "pmm remove %s", pkg);
ps_squote(uninst, e_un, sizeof(e_un));
FILE *f = fopen(ps1, "wb");
if (!f) return -1;
fprintf(f, "$ErrorActionPreference='SilentlyContinue'\r\n");
fprintf(f, "$k='HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\%s'\r\n", pkg);
fprintf(f, "New-Item -Path $k -Force | Out-Null\r\n");
fprintf(f, "Set-ItemProperty -Path $k -Name DisplayName -Value '%s' -Force\r\n", e_nm);
if (ver && *ver) fprintf(f, "Set-ItemProperty -Path $k -Name DisplayVersion -Value '%s' -Force\r\n", e_ver);
if (pub && *pub) fprintf(f, "Set-ItemProperty -Path $k -Name Publisher -Value '%s' -Force\r\n", e_pub);
if (icon && *icon) fprintf(f, "Set-ItemProperty -Path $k -Name DisplayIcon -Value '%s' -Force\r\n", e_icon);
fprintf(f, "Set-ItemProperty -Path $k -Name UninstallString -Value '%s' -Force\r\n", e_un);
if (install_location && *install_location)
fprintf(f, "Set-ItemProperty -Path $k -Name InstallLocation -Value '%s' -Force\r\n", e_loc);
fprintf(f, "New-ItemProperty -Path $k -Name EstimatedSize -Value %llu -PropertyType DWord -Force | Out-Null\r\n",
(unsigned long long)(size_bytes / 1024ULL));
{ /* InstallDate YYYYMMDD */
time_t t = time(NULL); struct tm *tm = localtime(&t);
char date[32];
if (tm) snprintf(date, sizeof(date), "%04d%02d%02d", tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday);
else snprintf(date, sizeof(date), "0");
fprintf(f, "Set-ItemProperty -Path $k -Name InstallDate -Value '%s' -Force\r\n", date);
}
fprintf(f, "New-ItemProperty -Path $k -Name NoModify -Value 1 -PropertyType DWord -Force | Out-Null\r\n");
fprintf(f, "New-ItemProperty -Path $k -Name NoRepair -Value 1 -PropertyType DWord -Force | Out-Null\r\n");
fclose(f);
char cmd[2600];
snprintf(cmd, sizeof(cmd),
"%%SystemRoot%%\\System32\\WindowsPowerShell\\v1.0\\powershell.exe -NoProfile -ExecutionPolicy Bypass -File \"%s\" >nul 2>&1",
ps1);
int rc = system(cmd);
remove(ps1);
if (rc != 0)
pmm_warn("%s", pmm_tr_fmt("msg.warn.no-register", pkg));
else
pmm_success("%s", pmm_tr_fmt("msg.registered", pkg, ver ? ver : "", pkg));
return rc == 0 ? 0 : -1;
#endif
}
void pmm_reg_uninstall_clear(const char *pkg) {
#ifndef _WIN32
(void)pkg;
#else
if (!pkg || !*pkg) return;
char cmd[2200];
snprintf(cmd, sizeof(cmd),
"%%SystemRoot%%\\System32\\WindowsPowerShell\\v1.0\\powershell.exe -NoProfile -Command \"Remove-Item -Path 'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\%s' -Recurse -Force -ErrorAction SilentlyContinue\" 2>nul",
pkg);
system(cmd);
pmm_success("%s", pmm_tr_fmt("msg.unregistered", pkg));
#endif
}