• 全新单提交:清除含敏感信息的旧历史(sudo 口令、内网 IP、SSH 密钥路径、热点 SSID、设备 MAC) • 本地开发文档/测试记录/构建脚本全部移入 gitignored 的 devdocs/ 与 scripts/build/(不入库) • 保留:MeowOS 源码(launcher/watchdog/bootmenu/meow_svcd/SDK)、项目规范文档、mips-repo 增补组件:SDK 统一日志+通知 API、meow_svcd 服务管理器、mips-repo 软件仓库
1015 lines
39 KiB
C
1015 lines
39 KiB
C
/*
|
||
* nyai.c — MeowOS apt/yum 风格软件包管理 CLI(静态 MIPS32r2, musl, 无外部依赖)
|
||
*
|
||
* 与 meow_launcherd 的 meow_pkg_install()/export() 语义一致(ANALYSIS.md §18.38 / M2.12;
|
||
* APPS.md 附录 v2)。支持镜像源:
|
||
* - 源配置 MeowOS/opt/nyai/sources.list(每行一个源;local <路径> / http <url> /
|
||
* deb [trusted=yes] <uri> / file:// ; # 注释)。默认 local MeowOS/opt/repo。
|
||
* - 每源一个 Packages 索引(行式 Package/Version/Filename/Size/SHA256/Depends/Description),
|
||
* 由 `nyai index <目录>` 生成。
|
||
* - update 缓存索引到 MeowOS/opt/nyai/cache;install 按名称从索引取包(本地文件或 http 拉取),
|
||
* 处理 Depends(递归)、校验 SHA256 + app.json;同 name 先备份;解压 MeowAPPs/<pkg>/;
|
||
* 数据 init(MeowData/<pkg>/{data,cache})。exFAT 无符号链接 → 数据目录由环境变量注入,不建链接。
|
||
*
|
||
* 路径前缀默认 /usr/data/mnt/sd_0;环境变量 MEOWROOT 可覆盖(仅本地 qemu/原生测试用,真机不设)。
|
||
*/
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
#include <ctype.h>
|
||
#include <unistd.h>
|
||
#include <errno.h>
|
||
#include <dirent.h>
|
||
#include <sys/stat.h>
|
||
#include <sys/types.h>
|
||
|
||
#define NYAI_VERSION "0.2.0"
|
||
#define BUF 1024
|
||
#define MAX_SRC 16
|
||
#define MAX_PKG 256
|
||
#define MAX_DEPS 64
|
||
#define MAX_DEPTH 32
|
||
|
||
/* ---------------- 路径 ---------------- */
|
||
static const char *meow_root(void)
|
||
{
|
||
const char *r = getenv("MEOWROOT");
|
||
return (r && *r) ? r : "/usr/data/mnt/sd_0";
|
||
}
|
||
static void path_apps(char *out, int n, const char *pkg)
|
||
{ snprintf(out, n, "%s/MeowAPPs/%s", meow_root(), pkg ? pkg : ""); }
|
||
static void path_data(char *out, int n, const char *pkg, const char *sub)
|
||
{ snprintf(out, n, "%s/MeowData/%s/%s", meow_root(), pkg ? pkg : "", sub ? sub : ""); }
|
||
static void path_apps_root(char *out, int n){ snprintf(out,n,"%s/MeowAPPs", meow_root()); }
|
||
static void path_sources(char *out, int n)
|
||
{ snprintf(out,n,"%s/MeowOS/opt/nyai/sources.list", meow_root()); }
|
||
static void path_cache(char *out, int n)
|
||
{ snprintf(out,n,"%s/MeowOS/opt/nyai/cache", meow_root()); }
|
||
static void path_repo_default(char *out, int n)
|
||
{ snprintf(out,n,"%s/MeowOS/opt/repo", meow_root()); }
|
||
|
||
/* ---------------- 源结构 ---------------- */
|
||
typedef struct {
|
||
int http; /* 0=本地路径 1=http(s) */
|
||
char base[512]; /* 本地目录 或 http(s)://基址 */
|
||
char display[512]; /* 用于提示 */
|
||
} src_t;
|
||
|
||
/* ---------------- 索引包记录 ---------------- */
|
||
typedef struct {
|
||
char pkg[128];
|
||
char version[64];
|
||
char filename[256];
|
||
char sha256[128];
|
||
char depends[256];
|
||
char desc[256];
|
||
long long size;
|
||
int src_idx;
|
||
} pkgrec;
|
||
|
||
static src_t g_srcs[MAX_SRC];
|
||
static int g_nsrc = 0;
|
||
|
||
/* ---------------- 字符串工具 ---------------- */
|
||
static char *trim(char *s)
|
||
{
|
||
char *e;
|
||
while (*s==' '||*s=='\t') s++;
|
||
e = s + strlen(s) - 1;
|
||
while (e>=s && (*e=='\n'||*e=='\r'||*e==' '||*e=='\t')) *e-- = 0;
|
||
return s;
|
||
}
|
||
/* 去注释(# 到行尾) */
|
||
static void strip_comment(char *s)
|
||
{
|
||
char *p = strchr(s, '#');
|
||
if (p) *p = 0;
|
||
}
|
||
|
||
/* 包名合法:仅 [A-Za-z0-9._-] */
|
||
static int valid_pkg(const char *s)
|
||
{
|
||
if (!s || !*s) return 0;
|
||
for (; *s; s++)
|
||
if (!(isalnum((unsigned char)*s) || *s=='.' || *s=='-' || *s=='_')) return 0;
|
||
return 1;
|
||
}
|
||
static int unsafe_path(const char *s)
|
||
{
|
||
return s && strpbrk(s, " ;`$'\"&|<>") != NULL;
|
||
}
|
||
|
||
static int mkdir_p(const char *path)
|
||
{
|
||
char buf[BUF];
|
||
snprintf(buf, sizeof buf, "%s", path);
|
||
for (char *p = buf + 1; *p; p++) {
|
||
if (*p == '/') { *p = 0; mkdir(buf, 0755); *p = '/'; }
|
||
}
|
||
return mkdir(buf, 0755) == 0 || errno == EEXIST ? 0 : -1;
|
||
}
|
||
static void rm_rf(const char *path)
|
||
{
|
||
struct stat st;
|
||
if (lstat(path, &st) != 0) return;
|
||
if (S_ISDIR(st.st_mode)) {
|
||
DIR *d = opendir(path);
|
||
if (d) {
|
||
struct dirent *de;
|
||
while ((de = readdir(d))) {
|
||
if (!strcmp(de->d_name,".") || !strcmp(de->d_name,"..")) continue;
|
||
char p[BUF]; snprintf(p,sizeof p,"%s/%s",path,de->d_name);
|
||
rm_rf(p);
|
||
}
|
||
closedir(d);
|
||
}
|
||
rmdir(path);
|
||
} else {
|
||
remove(path);
|
||
}
|
||
}
|
||
static long long dir_size(const char *path)
|
||
{
|
||
struct stat st;
|
||
if (lstat(path, &st) != 0) return 0;
|
||
if (S_ISREG(st.st_mode)) return st.st_size;
|
||
if (!S_ISDIR(st.st_mode)) return 0;
|
||
DIR *d = opendir(path);
|
||
if (!d) return 0;
|
||
long long total = 0;
|
||
struct dirent *de;
|
||
while ((de = readdir(d))) {
|
||
if (!strcmp(de->d_name,".") || !strcmp(de->d_name,"..")) continue;
|
||
char p[BUF]; snprintf(p,sizeof p,"%s/%s",path,de->d_name);
|
||
long long s = dir_size(p);
|
||
if (s < 0) { closedir(d); return -1; }
|
||
total += s;
|
||
}
|
||
closedir(d);
|
||
return total;
|
||
}
|
||
static void human_size(long long b, char *out, int n)
|
||
{
|
||
if (b < 1024) snprintf(out,n,"%lld B", b);
|
||
else if (b < 1024*1024) snprintf(out,n,"%.1f KB", b/1024.0);
|
||
else snprintf(out,n,"%.2f MB", b/(1024.0*1024));
|
||
}
|
||
|
||
/* 简版版本比较(点/横分段,数字段按数值):a>b 返回 1,=0,< -1 */
|
||
static int version_cmp(const char *a, const char *b)
|
||
{
|
||
const char *x=a,*y=b;
|
||
while (*x || *y) {
|
||
while (*x=='.'||*x=='-') x++;
|
||
while (*y=='.'||*y=='-') y++;
|
||
if (!*x || !*y) break;
|
||
if (isdigit((unsigned char)*x) && isdigit((unsigned char)*y)) {
|
||
long nx=0, ny=0;
|
||
while (isdigit((unsigned char)*x)) nx=nx*10+(*x++ - '0');
|
||
while (isdigit((unsigned char)*y)) ny=ny*10+(*y++ - '0');
|
||
if (nx!=ny) return nx<ny?-1:1;
|
||
} else {
|
||
if (*x!=*y) return *x<*y?-1:1;
|
||
x++; y++;
|
||
}
|
||
}
|
||
if (!*x && !*y) return 0;
|
||
return *x ? 1 : -1;
|
||
}
|
||
|
||
/* ---------------- JSON 读取 ---------------- */
|
||
static int json_str(const char *j, const char *key, char *out, int n)
|
||
{
|
||
if (!j || !key) return -1;
|
||
char pat[160];
|
||
snprintf(pat,sizeof pat,"\"%s\"", key);
|
||
const char *p = j;
|
||
while ((p = strstr(p, pat)) != NULL) {
|
||
p += strlen(pat);
|
||
while (*p==' '||*p=='\t'||*p=='\n'||*p=='\r') p++;
|
||
if (*p != ':') { p++; continue; }
|
||
p++;
|
||
while (*p==' '||*p=='\t'||*p=='\n'||*p=='\r') p++;
|
||
if (*p != '"') { p++; continue; }
|
||
p++;
|
||
int o=0;
|
||
while (*p && *p != '"') {
|
||
if (*p=='\\' && p[1]) { if (o<n-1) out[o++]=p[1]; p+=2; continue; }
|
||
if (o<n-1) out[o++]=*p;
|
||
p++;
|
||
}
|
||
out[o]=0;
|
||
return 0;
|
||
}
|
||
return -1;
|
||
}
|
||
static int json_raw(const char *j, const char *key, char *out, int n)
|
||
{
|
||
if (!j || !key) return -1;
|
||
char pat[160];
|
||
snprintf(pat,sizeof pat,"\"%s\"", key);
|
||
const char *p = strstr(j, pat);
|
||
if (!p) return -1;
|
||
p += strlen(pat);
|
||
while (*p && *p != ':') p++;
|
||
if (*p != ':') return -1;
|
||
p++;
|
||
while (*p && (*p==' '||*p=='\t'||*p=='\n'||*p=='\r')) p++;
|
||
if (!*p) return -1;
|
||
char open=*p, close=0;
|
||
int depth=0, in_str=0, o=0;
|
||
if (open=='[') close=']'; else if (open=='{') close='}';
|
||
while (*p) {
|
||
if (in_str) {
|
||
if (o<n-1) out[o++]=*p;
|
||
if (*p=='\\' && p[1]) { if (o<n-1) out[o++]=p[1]; p+=2; continue; }
|
||
if (*p=='"') in_str=0;
|
||
p++; continue;
|
||
}
|
||
if (*p=='"') { in_str=1; if (o<n-1) out[o++]=*p; p++; continue; }
|
||
if (open=='['||open=='{') {
|
||
if (*p==open) depth++;
|
||
else if (*p==close) { depth--; if (o<n-1) out[o++]=*p; p++; if (depth==0) break; continue; }
|
||
if (o<n-1) out[o++]=*p;
|
||
p++; continue;
|
||
}
|
||
if (*p==','||*p=='\n'||*p=='\r'||*p=='}'||*p==']') break;
|
||
if (o<n-1) out[o++]=*p;
|
||
p++;
|
||
}
|
||
out[o]=0;
|
||
return out[0] ? 0 : -1;
|
||
}
|
||
|
||
typedef struct {
|
||
char name[256];
|
||
char version[64];
|
||
char exec[512];
|
||
char author[128];
|
||
char routes[512];
|
||
char services[512];
|
||
char depends[256];
|
||
char description[256];
|
||
int has_routes, has_services;
|
||
} appmeta;
|
||
|
||
static int load_app_meta(const char *path, appmeta *m)
|
||
{
|
||
memset(m, 0, sizeof *m);
|
||
FILE *f = fopen(path, "r");
|
||
if (!f) return -1;
|
||
static char j[64*1024];
|
||
size_t n = fread(j, 1, sizeof j - 1, f);
|
||
fclose(f);
|
||
j[n] = 0;
|
||
int bal=0;
|
||
for (size_t i=0;i<n;i++){ if (j[i]=='{') bal++; else if (j[i]=='}') bal--; if (bal<0) return -1; }
|
||
if (bal != 0) return -1;
|
||
if (json_str(j,"name",m->name,sizeof m->name)!=0 || !m->name[0]) return -1;
|
||
if (json_str(j,"exec",m->exec,sizeof m->exec)!=0 || !m->exec[0]) return -1;
|
||
if (json_str(j,"version",m->version,sizeof m->version)!=0) m->version[0]=0;
|
||
if (json_str(j,"author",m->author,sizeof m->author)!=0) m->author[0]=0;
|
||
if (json_str(j,"depends",m->depends,sizeof m->depends)!=0) m->depends[0]=0;
|
||
if (json_str(j,"description",m->description,sizeof m->description)!=0) m->description[0]=0;
|
||
m->has_routes = json_raw(j,"routes",m->routes,sizeof m->routes)==0;
|
||
m->has_services = json_raw(j,"services",m->services,sizeof m->services)==0;
|
||
return 0;
|
||
}
|
||
|
||
/* 从 .mpk 里尽力读 app.json 字段(name/version/depends/description)。
|
||
* 用 tar -t 定位成员,再 tar -xO 取内容;失败返回 -1。 */
|
||
static int mpk_appinfo(const char *file, appmeta *m)
|
||
{
|
||
memset(m, 0, sizeof *m);
|
||
char cmd[1024];
|
||
snprintf(cmd,sizeof cmd,
|
||
"gzip -dc '%s' 2>/dev/null | tar -t 2>/dev/null | grep 'app\\.json$'", file);
|
||
FILE *pf = popen(cmd, "r");
|
||
if (!pf) return -1;
|
||
char member[256]; member[0]=0;
|
||
if (fgets(member,sizeof member,pf)==NULL) { pclose(pf); return -1; }
|
||
char *mm = member; while (*mm==' '||*mm=='\t') mm++;
|
||
size_t mlen=strlen(mm); while (mlen&&(mm[mlen-1]=='\n'||mm[mlen-1]=='\r'||mm[mlen-1]==' ')) mm[--mlen]=0;
|
||
pclose(pf);
|
||
if (!mlen) return -1;
|
||
snprintf(cmd,sizeof cmd,"gzip -dc '%s' 2>/dev/null | tar -xO '%s' 2>/dev/null", file, mm);
|
||
pf = popen(cmd, "r");
|
||
if (!pf) return -1;
|
||
static char j[16384];
|
||
size_t r = fread(j,1,sizeof j-1,pf);
|
||
pclose(pf);
|
||
if (!r) return -1;
|
||
j[r]=0;
|
||
if (json_str(j,"name",m->name,sizeof m->name)!=0 || !m->name[0]) return -1;
|
||
if (json_str(j,"version",m->version,sizeof m->version)!=0) m->version[0]=0;
|
||
if (json_str(j,"depends",m->depends,sizeof m->depends)!=0) m->depends[0]=0;
|
||
if (json_str(j,"description",m->description,sizeof m->description)!=0) m->description[0]=0;
|
||
return 0;
|
||
}
|
||
|
||
/* SHA256(外部 sha256sum,busybox/宿主均有);失败返回 -1 */
|
||
static int sha256_file(const char *path, char *out, int n)
|
||
{
|
||
if (unsafe_path(path)) return -1;
|
||
char cmd[BUF];
|
||
snprintf(cmd,sizeof cmd,"sha256sum '%s' 2>/dev/null", path);
|
||
FILE *pf = popen(cmd,"r");
|
||
if (!pf) return -1;
|
||
char line[256];
|
||
if (fgets(line,sizeof line,pf)==NULL) { pclose(pf); return -1; }
|
||
pclose(pf);
|
||
char *h=line;
|
||
/* sha256sum 输出为 "<hash> <filename>",仅取第一个空白前 token */
|
||
for (char *q=h; *q; q++) { if (*q==' '||*q=='\t') { *q=0; break; } }
|
||
size_t L = strlen(h);
|
||
if (L != 64) return -1;
|
||
if (n>64) strcpy(out,h); else return -1;
|
||
return 0;
|
||
}
|
||
|
||
/* ---------------- 源加载 ---------------- */
|
||
static int curl_available(void)
|
||
{
|
||
if (access("/usr/data/mnt/sd_0/MeowOS/opt/bin/curl",X_OK)==0) return 1;
|
||
if (access("/usr/bin/curl",X_OK)==0) return 1;
|
||
return 0;
|
||
}
|
||
static const char *curl_bin(void)
|
||
{
|
||
if (access("/usr/data/mnt/sd_0/MeowOS/opt/bin/curl",X_OK)==0) return "/usr/data/mnt/sd_0/MeowOS/opt/bin/curl";
|
||
if (access("/usr/bin/curl",X_OK)==0) return "/usr/bin/curl";
|
||
return "curl";
|
||
}
|
||
|
||
/* 规范化一条源记录(去掉 file://、deb [..] 前缀),返回 0 */
|
||
static int normalize_src(const char *raw, src_t *s)
|
||
{
|
||
char line[600];
|
||
snprintf(line,sizeof line,"%s", raw);
|
||
strip_comment(line);
|
||
char *t = trim(line);
|
||
if (!*t) return -1;
|
||
/* deb [trusted=yes] <uri> */
|
||
if (!strncmp(t,"deb ", 4)) {
|
||
t = trim(t + 4);
|
||
/* 去掉 [ ... ] */
|
||
char *br = strchr(t,'[');
|
||
if (br) { char *b2 = strchr(br,']'); if (b2) memmove(br, b2+1, strlen(b2+1)+1); t = trim(t); }
|
||
} else if (!strncmp(t,"local ", 6)) {
|
||
t = trim(t + 6);
|
||
} else if (!strncmp(t,"http ", 5)) {
|
||
t = trim(t + 5);
|
||
}
|
||
if (!*t) return -1;
|
||
snprintf(s->base, sizeof s->base, "%s", t);
|
||
snprintf(s->display,sizeof s->display,"%s", t);
|
||
/* file:// 前缀 → 本地路径 */
|
||
if (!strncmp(s->base,"file://", 7)) {
|
||
memmove(s->base, s->base+7, strlen(s->base+7)+1);
|
||
s->http = 0;
|
||
} else if (!strncmp(s->base,"http://",7) || !strncmp(s->base,"https://",8)) {
|
||
s->http = 1;
|
||
} else {
|
||
s->http = 0;
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
static int load_sources(void)
|
||
{
|
||
g_nsrc = 0;
|
||
char sp[BUF];
|
||
path_sources(sp,sizeof sp);
|
||
FILE *f = fopen(sp, "r");
|
||
if (f) {
|
||
char line[600];
|
||
while (fgets(line,sizeof line,f) && g_nsrc < MAX_SRC) {
|
||
src_t s;
|
||
if (normalize_src(line,&s)==0) g_srcs[g_nsrc++] = s;
|
||
}
|
||
fclose(f);
|
||
}
|
||
if (g_nsrc == 0) {
|
||
/* 缺省源:local MeowOS/opt/repo */
|
||
char d[BUF]; path_repo_default(d,sizeof d);
|
||
snprintf(g_srcs[0].base,sizeof g_srcs[0].base,"%s", d);
|
||
snprintf(g_srcs[0].display,sizeof g_srcs[0].display,"%s", d);
|
||
g_srcs[0].http = 0;
|
||
g_nsrc = 1;
|
||
printf("nyai: 无 sources.list,使用默认本地源 %s\n", d);
|
||
}
|
||
return g_nsrc;
|
||
}
|
||
|
||
/* 某源的索引文件路径:本地 = <base>/Packages;http = <base>/Packages(远端),本地缓存 = cache/src<i> */
|
||
static int source_index_local(const src_t *s, int idx, char *out, int n)
|
||
{
|
||
if (s->http) {
|
||
/* 缓存的远端索引 */
|
||
char c[BUF]; path_cache(c,sizeof c);
|
||
snprintf(out,n,"%s/src%d-Packages", c, idx);
|
||
return 0;
|
||
}
|
||
snprintf(out,n,"%s/Packages", s->base);
|
||
return 0;
|
||
}
|
||
static void source_index_url(const src_t *s, char *out, int n)
|
||
{
|
||
snprintf(out,n,"%s/Packages", s->base);
|
||
}
|
||
static void source_file_local(const src_t *s, const char *filename, char *out, int n)
|
||
{
|
||
snprintf(out,n,"%s/%s", s->base, filename);
|
||
}
|
||
static void source_file_url(const src_t *s, const char *filename, char *out, int n)
|
||
{
|
||
snprintf(out,n,"%s/%s", s->base, filename);
|
||
}
|
||
|
||
/* ---------------- 索引解析 ---------------- */
|
||
/* 从文件读 Packages 索引到 recs;返回条数 */
|
||
static int parse_index(const char *path, pkgrec *recs, int max)
|
||
{
|
||
FILE *f = fopen(path,"r");
|
||
if (!f) return 0;
|
||
int n = 0;
|
||
pkgrec cur; memset(&cur,0,sizeof cur); int have = 0;
|
||
char line[600];
|
||
while (fgets(line,sizeof line,f)) {
|
||
char *t = trim(line);
|
||
if (!*t) { /* 空行 => 终结一条 */
|
||
if (have && n < max) { recs[n++] = cur; }
|
||
memset(&cur,0,sizeof cur); have = 0;
|
||
continue;
|
||
}
|
||
char *colon = strchr(t, ':');
|
||
if (!colon) continue;
|
||
*colon = 0;
|
||
char *key = trim(t), *val = trim(colon+1);
|
||
if (!strcmp(key,"Package")) { snprintf(cur.pkg,sizeof cur.pkg,"%s",val); have = (cur.pkg[0]!=0); }
|
||
else if (!strcmp(key,"Version")) snprintf(cur.version,sizeof cur.version,"%s",val);
|
||
else if (!strcmp(key,"Filename")) snprintf(cur.filename,sizeof cur.filename,"%s",val);
|
||
else if (!strcmp(key,"SHA256")) snprintf(cur.sha256,sizeof cur.sha256,"%s",val);
|
||
else if (!strcmp(key,"Size")) { cur.size = atoll(val); }
|
||
else if (!strcmp(key,"Depends")) snprintf(cur.depends,sizeof cur.depends,"%s",val);
|
||
else if (!strcmp(key,"Description")) snprintf(cur.desc,sizeof cur.desc,"%s",val);
|
||
}
|
||
if (have && n < max) recs[n++] = cur;
|
||
fclose(f);
|
||
return n;
|
||
}
|
||
|
||
/* 加载所有缓存索引(update 后);把 src_idx 标注好 */
|
||
static int load_all_indexes(pkgrec *recs, int max, const src_t *srcs, int nsrc)
|
||
{
|
||
int n=0;
|
||
for (int i=0;i<nsrc && n<max;i++) {
|
||
char p[BUF];
|
||
source_index_local(&srcs[i], i, p, sizeof p);
|
||
int k = parse_index(p, recs+n, max-n);
|
||
for (int j=0;j<k;j++) recs[n+j].src_idx = i;
|
||
n += k;
|
||
}
|
||
return n;
|
||
}
|
||
|
||
/* ---------------- 依赖解析/安装(递归) ---------------- */
|
||
static int pkg_installed(const char *pkg)
|
||
{
|
||
char p[BUF];
|
||
snprintf(p,sizeof p,"%s/MeowAPPs/%s/app.json", meow_root(), pkg);
|
||
return access(p, R_OK) == 0;
|
||
}
|
||
|
||
static int install_mpk_file(const char *mpk_path);
|
||
|
||
/* 按记录安装一个索引包(含 sha256 校验 + 依赖),done 记录已装避免循环 */
|
||
static int install_rec(pkgrec *r, pkgrec *recs, int nrec, char (*done)[128], int ndone, int depth);
|
||
|
||
/* 收集依赖并逐个安装(用于 install_by_name) */
|
||
static int install_deps_of(pkgrec *r, pkgrec *recs, int nrec, char (*done)[128], int *ndone, int depth)
|
||
{
|
||
if (depth > MAX_DEPTH) return -1;
|
||
if (!r->depends[0]) return 0;
|
||
char d[512]; snprintf(d,sizeof d,"%s", r->depends);
|
||
char *save=NULL, *tok = strtok_r(d, ", ", &save);
|
||
while (tok) {
|
||
tok = trim(tok);
|
||
if (*tok && valid_pkg(tok)) {
|
||
int seen=0;
|
||
for (int i=0;i<*ndone;i++) if (!strcmp(done[i],tok)) { seen=1; break; }
|
||
if (!seen && !pkg_installed(tok)) {
|
||
int idx=-1;
|
||
for (int i=0;i<nrec;i++) if (!strcmp(recs[i].pkg,tok)) { idx=i; break; }
|
||
if (idx<0) {
|
||
fprintf(stderr,"nyai: 警告: 依赖 %s 未在当前源找到\n", tok);
|
||
} else {
|
||
int rc = install_rec(&recs[idx], recs, nrec, done, *ndone, depth+1);
|
||
if (rc != 0) { fprintf(stderr,"nyai: 依赖 %s 安装失败\n", tok); return -1; }
|
||
}
|
||
}
|
||
}
|
||
tok = strtok_r(NULL, ", ", &save);
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
/* 按名称从索引安装 */
|
||
static int install_by_name(const char *name, pkgrec *recs, int nrec)
|
||
{
|
||
int idx=-1;
|
||
for (int i=0;i<nrec;i++) if (!strcmp(recs[i].pkg,name)) { idx=i; break; }
|
||
if (idx<0) {
|
||
fprintf(stderr,"nyai: 未找到包 %s(先 nyai update 刷新索引)\n", name);
|
||
return 1;
|
||
}
|
||
char done[MAX_DEPS][128];
|
||
int ndone=0;
|
||
return install_rec(&recs[idx], recs, nrec, done, ndone, 0);
|
||
}
|
||
|
||
static int install_rec(pkgrec *r, pkgrec *recs, int nrec, char (*done)[128], int ndone, int depth)
|
||
{
|
||
/* 记录正在处理的包,防循环 */
|
||
if (ndone < MAX_DEPS) { snprintf(done[ndone],128,"%s", r->pkg); ndone++; }
|
||
/* 先装依赖 */
|
||
if (install_deps_of(r, recs, nrec, done, &ndone, depth+1) != 0) return 1;
|
||
|
||
/* 取本地路径(本地源用文件;http 源 curl 下载到 cache) */
|
||
src_t *s = &g_srcs[r->src_idx];
|
||
char local[BUF], tmp[BUF];
|
||
if (s->http) {
|
||
char c[BUF]; path_cache(c,sizeof c); mkdir_p(c);
|
||
char url[BUF]; source_file_url(s, r->filename, url, sizeof url);
|
||
snprintf(tmp,sizeof tmp,"%s/dl_%d", c, rand());
|
||
snprintf(local,sizeof local,"%s", tmp);
|
||
if (!curl_available()) {
|
||
fprintf(stderr,"nyai: http 源 %s 需要 curl(卡上 MeowOS/opt/bin/curl),当前不可用\n", s->display);
|
||
return 1;
|
||
}
|
||
char cmd[512];
|
||
snprintf(cmd,sizeof cmd,"%s -fsSL --connect-timeout 3 --max-time 8 '%s' -o '%s' 2>/dev/null", curl_bin(), url, tmp);
|
||
printf(" Fetch %s ...\n", url);
|
||
if (system(cmd)!=0) { fprintf(stderr,"nyai: 拉取 %s 失败(无网络?)\n", url); remove(tmp); return 1; }
|
||
} else {
|
||
source_file_local(s, r->filename, local, sizeof local);
|
||
if (access(local, R_OK)!=0) {
|
||
fprintf(stderr,"nyai: 源 %s 缺包文件 %s\n", s->display, local);
|
||
return 1;
|
||
}
|
||
}
|
||
/* SHA256 校验 */
|
||
if (r->sha256[0] && strlen(r->sha256)==64) {
|
||
char got[128];
|
||
if (sha256_file(local, got, sizeof got)!=0) {
|
||
fprintf(stderr,"nyai: 无法计算 %s 的 SHA256(需 sha256sum)\n", local);
|
||
if (s->http) remove(local);
|
||
return 1;
|
||
}
|
||
if (strcmp(got, r->sha256)!=0) {
|
||
fprintf(stderr,"nyai: SHA256 校验失败: %s\n got %s\n want %s\n", local, got, r->sha256);
|
||
if (s->http) { remove(local); }
|
||
return 1;
|
||
}
|
||
}
|
||
int rc = install_mpk_file(local);
|
||
if (s->http) remove(local);
|
||
return rc;
|
||
}
|
||
|
||
/* ---------------- update ---------------- */
|
||
static int cmd_update(void)
|
||
{
|
||
load_sources();
|
||
printf("Reading package lists...\n");
|
||
char c[BUF]; path_cache(c,sizeof c); mkdir_p(c);
|
||
int ok = 0;
|
||
for (int i=0;i<g_nsrc;i++) {
|
||
src_t *s = &g_srcs[i];
|
||
printf("Get: [%d] %s", i+1, s->display);
|
||
char idxlocal[BUF];
|
||
source_index_local(s, i, idxlocal, sizeof idxlocal);
|
||
if (s->http) {
|
||
if (!curl_available()) { printf(" -- 无 curl,跳过 (http 源不可用)\n"); continue; }
|
||
char url[BUF]; source_index_url(s, url, sizeof url);
|
||
printf(" -> %s\n", url);
|
||
char cmd[512];
|
||
snprintf(cmd,sizeof cmd,"%s -fsSL --connect-timeout 3 --max-time 8 '%s' -o '%s' 2>/dev/null", curl_bin(), url, idxlocal);
|
||
if (system(cmd)!=0) {
|
||
printf(" nyai: 更新 %s 失败(无网络/源不可达),保留旧缓存\n", s->display);
|
||
continue; /* 保留旧缓存,不硬失败 */
|
||
}
|
||
ok++;
|
||
} else {
|
||
if (access(idxlocal, R_OK)==0) { printf(" ok\n"); ok++; }
|
||
else printf(" -- 无 Packages 索引(用 nyai index <dir> 生成)\n");
|
||
}
|
||
}
|
||
printf("Done.\n");
|
||
return ok ? 0 : 1;
|
||
}
|
||
|
||
/* ---------------- index <dir> ---------------- */
|
||
static int cmd_index(const char *dir)
|
||
{
|
||
if (!dir || !*dir) { fprintf(stderr,"nyai: index: 缺目录\n"); return 2; }
|
||
char r[BUF]; snprintf(r,sizeof r,"%s", dir);
|
||
if (access(r, R_OK)!=0) { fprintf(stderr,"nyai: index: 目录 %s 不可读\n", r); return 1; }
|
||
DIR *d = opendir(r);
|
||
if (!d) { fprintf(stderr,"nyai: index: 无法打开 %s\n", r); return 1; }
|
||
char out[BUF]; snprintf(out,sizeof out,"%s/Packages", r);
|
||
FILE *f = fopen(out, "w");
|
||
if (!f) { fprintf(stderr,"nyai: index: 无法写 %s\n", out); closedir(d); return 1; }
|
||
printf("Generating index for %s ...\n", r);
|
||
struct dirent *de;
|
||
int n = 0;
|
||
while ((de = readdir(d))) {
|
||
if (de->d_name[0]=='.') continue;
|
||
size_t L = strlen(de->d_name);
|
||
if (L<4 || strcmp(de->d_name+L-4, ".mpk")!=0) continue;
|
||
char fpath[BUF]; snprintf(fpath,sizeof fpath,"%s/%s", r, de->d_name);
|
||
struct stat st; if (stat(fpath,&st)!=0) continue;
|
||
char stem[128]; snprintf(stem,sizeof stem,"%.*s", (int)(L-4), de->d_name);
|
||
appmeta m;
|
||
char ver[64]="", dep[256]="", desc[256]="";
|
||
if (mpk_appinfo(fpath,&m)==0) {
|
||
snprintf(ver,sizeof ver,"%s", m.version); snprintf(dep,sizeof dep,"%s", m.depends);
|
||
snprintf(desc,sizeof desc,"%s", m.description[0]?m.description:m.name);
|
||
}
|
||
char sha[128]=""; sha256_file(fpath, sha, sizeof sha);
|
||
fprintf(f,"Package: %s\n", stem);
|
||
fprintf(f,"Version: %s\n", ver[0]?ver:"0.0.0");
|
||
fprintf(f,"Filename: %s\n", de->d_name);
|
||
fprintf(f,"Size: %lld\n", (long long)st.st_size);
|
||
if (sha[0]) fprintf(f,"SHA256: %s\n", sha);
|
||
if (dep[0]) fprintf(f,"Depends: %s\n", dep);
|
||
if (desc[0]) fprintf(f,"Description: %s\n", desc);
|
||
fprintf(f,"\n");
|
||
printf(" %s (%s)\n", stem, ver[0]?ver:"0.0.0");
|
||
n++;
|
||
}
|
||
fclose(f);
|
||
closedir(d);
|
||
printf("OK: %d package(s) indexed -> %s\n", n, out);
|
||
return 0;
|
||
}
|
||
|
||
/* ---------------- search ---------------- */
|
||
static int cmd_search(const char *pat, pkgrec *recs, int nrec)
|
||
{
|
||
int n=0;
|
||
printf("%-24s %-12s %s\n","Package","Version","Description");
|
||
printf("%-24s %-12s %s\n","-------","-------","-----------");
|
||
for (int i=0;i<nrec;i++) {
|
||
if (pat && *pat) {
|
||
if (!strstr(recs[i].pkg, pat) && !strstr(recs[i].desc, pat) && !strstr(recs[i].version, pat))
|
||
continue;
|
||
}
|
||
printf("%-24s %-12s %s\n", recs[i].pkg, recs[i].version[0]?recs[i].version:"?",
|
||
recs[i].desc[0]?recs[i].desc:"");
|
||
n++;
|
||
}
|
||
if (!n) printf("No match.\n");
|
||
else printf("\n%d package(s) found.\n", n);
|
||
return 0;
|
||
}
|
||
|
||
/* ---------------- list-upgradable ---------------- */
|
||
static int cmd_list_upgradable(pkgrec *recs, int nrec)
|
||
{
|
||
char r[BUF]; snprintf(r,sizeof r,"%s/MeowAPPs", meow_root());
|
||
DIR *d = opendir(r);
|
||
if (!d) { fprintf(stderr,"nyai: 无法打开 MeowAPPs\n"); return 1; }
|
||
int n=0;
|
||
printf("%-24s %-12s %-12s\n","Package","Installed","Available");
|
||
struct dirent *de;
|
||
while ((de = readdir(d))) {
|
||
if (de->d_name[0]=='.') continue;
|
||
if (!strcmp(de->d_name,"fonts")||!strcmp(de->d_name,"logs")||
|
||
!strcmp(de->d_name,"backgrounds")||!strcmp(de->d_name,"meow.conf")||
|
||
strstr(de->d_name,".bak_")) continue;
|
||
for (int i=0;i<nrec;i++) {
|
||
if (!strcmp(recs[i].pkg, de->d_name)) {
|
||
char ap[BUF]; snprintf(ap,sizeof ap,"%s/%s/app.json", r, de->d_name);
|
||
appmeta m;
|
||
if (load_app_meta(ap,&m)!=0) break;
|
||
if (version_cmp(recs[i].version, m.version) > 0) {
|
||
printf("%-24s %-12s %-12s\n", de->d_name, m.version[0]?m.version:"?", recs[i].version);
|
||
n++;
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
closedir(d);
|
||
if (!n) printf("All packages up to date.\n");
|
||
else printf("\n%d package(s) can be upgraded.\n", n);
|
||
return 0;
|
||
}
|
||
|
||
/* ---------------- install(文件或名称) ---------------- */
|
||
static int install_mpk_file(const char *mpk_path)
|
||
{
|
||
if (!mpk_path || !*mpk_path) { fprintf(stderr,"nyai: install: 缺 .mpk 路径\n"); return 2; }
|
||
if (access(mpk_path, R_OK)!=0) { fprintf(stderr,"nyai: install: 打不开 %s\n", mpk_path); return 1; }
|
||
if (unsafe_path(mpk_path)) { fprintf(stderr,"nyai: install: 路径含非法字符\n"); return 1; }
|
||
const char *bn = strrchr(mpk_path,'/');
|
||
bn = bn ? bn+1 : mpk_path;
|
||
const char *dot = strrchr(bn,'.');
|
||
if (!dot || strcmp(dot,".mpk")!=0) { fprintf(stderr,"nyai: install: 需要 <pkg>.mpk\n"); return 1; }
|
||
char pkg[64];
|
||
int len=(int)(dot-bn);
|
||
if (len<=0 || len>=(int)sizeof pkg) { fprintf(stderr,"nyai: install: 包名无效\n"); return 1; }
|
||
memcpy(pkg,bn,len); pkg[len]=0;
|
||
if (!valid_pkg(pkg)) { fprintf(stderr,"nyai: install: 包名含非法字符: %s\n", pkg); return 1; }
|
||
|
||
printf("Reading package lists... Done\n");
|
||
char dst[BUF], bak[BUF];
|
||
snprintf(dst,sizeof dst,"%s/MeowAPPs/%s", meow_root(), pkg);
|
||
snprintf(bak,sizeof bak,"%s.bak_%d", dst, (int)getpid());
|
||
int replacing = (access(dst,F_OK)==0);
|
||
if (replacing) {
|
||
rm_rf(bak);
|
||
if (rename(dst,bak)!=0) { fprintf(stderr,"nyai: install: 备份 %s 失败: %s\n", dst, strerror(errno)); return 1; }
|
||
printf("Backing up existing %s -> %s\n", pkg, bak);
|
||
}
|
||
printf("Preparing to unpack %s ...\n", pkg);
|
||
if (mkdir_p(dst)!=0) { if (replacing) rename(bak,dst); return 1; }
|
||
char cmd[1024];
|
||
snprintf(cmd,sizeof cmd,"gzip -dc '%s' | tar -x -C '%s' 2>/dev/null && [ -f '%s/app.json' ]", mpk_path, dst, dst);
|
||
printf("Unpacking %s ...\n", pkg);
|
||
if (system(cmd)!=0) {
|
||
fprintf(stderr,"nyai: install: 解压/校验 app.json 失败(%s 可能损坏或非 .mpk)\n", mpk_path);
|
||
rm_rf(dst); if (replacing) rename(bak,dst); return 1;
|
||
}
|
||
char appj[BUF]; snprintf(appj,sizeof appj,"%s/app.json", dst);
|
||
appmeta m;
|
||
if (load_app_meta(appj,&m)!=0) {
|
||
fprintf(stderr,"nyai: install: app.json 不合法(缺 name 或 exec)\n");
|
||
rm_rf(dst); if (replacing) rename(bak,dst); return 1;
|
||
}
|
||
char d1[BUF],d2[BUF];
|
||
path_data(d1,sizeof d1,pkg,"data"); path_data(d2,sizeof d2,pkg,"cache");
|
||
mkdir_p(d1); mkdir_p(d2);
|
||
printf("Setting up %s (%s) ...\n", pkg, m.version[0]?m.version:"?");
|
||
printf("OK\n");
|
||
return 0;
|
||
}
|
||
|
||
static int cmd_install(const char *arg)
|
||
{
|
||
if (!arg || !*arg) { fprintf(stderr,"nyai: install: 缺包名或 .mpk\n"); return 2; }
|
||
/* ① 文件路径直接装 */
|
||
size_t L=strlen(arg);
|
||
if ((L>=4 && !strcmp(arg+L-4,".mpk")) && access(arg,R_OK)==0) {
|
||
return install_mpk_file(arg);
|
||
}
|
||
if (!valid_pkg(arg)) {
|
||
fprintf(stderr,"nyai: install: 非法包名: %s\n", arg);
|
||
return 1;
|
||
}
|
||
/* ② 按索引装 */
|
||
pkgrec recs[MAX_PKG];
|
||
int nrec = load_all_indexes(recs, MAX_PKG, g_srcs, g_nsrc);
|
||
if (nrec == 0) {
|
||
fprintf(stderr,"nyai: 索引为空,先 nyai update 刷新(或直接 install <file>.mpk)\n");
|
||
return 1;
|
||
}
|
||
return install_by_name(arg, recs, nrec);
|
||
}
|
||
|
||
/* ---------------- list / info / remove / export / scan / upgrade ---------------- */
|
||
static int cmd_list(void)
|
||
{
|
||
char r[BUF]; snprintf(r,sizeof r,"%s/MeowAPPs", meow_root());
|
||
DIR *d = opendir(r);
|
||
if (!d) { fprintf(stderr,"nyai: list: 打开 %s 失败\n", r); return 1; }
|
||
int found=0;
|
||
printf("%-24s %-10s %-10s %s\n","Name","Version","Size","ID");
|
||
printf("%-24s %-10s %-10s %s\n","----","-------","----","--");
|
||
struct dirent *de;
|
||
while ((de=readdir(d))) {
|
||
if (de->d_name[0]=='.') continue;
|
||
if (!strcmp(de->d_name,"fonts")||!strcmp(de->d_name,"logs")||
|
||
!strcmp(de->d_name,"backgrounds")||!strcmp(de->d_name,"meow.conf")||
|
||
!strcmp(de->d_name,"meow") || strstr(de->d_name,".bak_")) continue;
|
||
char didx[BUF],dmeta[BUF];
|
||
snprintf(didx,sizeof didx,"%s/%s", r, de->d_name);
|
||
snprintf(dmeta,sizeof dmeta,"%s/app.json", didx);
|
||
appmeta m;
|
||
if (load_app_meta(dmeta,&m)!=0) continue;
|
||
long long sz=dir_size(didx);
|
||
char hs[24]; human_size(sz,hs,sizeof hs);
|
||
printf("%-24s %-10s %-10s %s\n", m.name[0]?m.name:de->d_name, m.version[0]?m.version:"?", hs, de->d_name);
|
||
found++;
|
||
}
|
||
closedir(d);
|
||
if (!found) printf("No packages found in MeowAPPs.\n");
|
||
else printf("\n%d package(s) installed.\n", found);
|
||
return 0;
|
||
}
|
||
|
||
/* 移除 MeowAPPs/<pkg>.bak_<pid> 类残留备份(install 升级/覆盖时产生) */
|
||
static void remove_pkg_backups(const char *pkg)
|
||
{
|
||
char r[BUF]; snprintf(r,sizeof r,"%s/MeowAPPs", meow_root());
|
||
DIR *d = opendir(r);
|
||
if (!d) return;
|
||
struct dirent *de;
|
||
/* 前缀 <pkg>.bak_ */
|
||
char pref[140]; snprintf(pref,sizeof pref,"%s.bak_", pkg);
|
||
size_t plen = strlen(pref);
|
||
while ((de = readdir(d))) {
|
||
if (strncmp(de->d_name, pref, plen)==0) {
|
||
char p[BUF]; snprintf(p,sizeof p,"%s/%s", r, de->d_name);
|
||
rm_rf(p);
|
||
}
|
||
}
|
||
closedir(d);
|
||
}
|
||
|
||
static int cmd_info(const char *pkg)
|
||
{
|
||
if (!pkg || !valid_pkg(pkg)) { fprintf(stderr,"nyai: info: 包名无效\n"); return 2; }
|
||
char dirp[BUF],appj[BUF];
|
||
snprintf(dirp,sizeof dirp,"%s/MeowAPPs/%s", meow_root(), pkg);
|
||
snprintf(appj,sizeof appj,"%s/app.json", dirp);
|
||
if (access(appj,R_OK)!=0) { fprintf(stderr,"nyai: info: 未安装 %s\n", pkg); return 1; }
|
||
appmeta m;
|
||
if (load_app_meta(appj,&m)!=0) { fprintf(stderr,"nyai: info: %s/app.json 不合法\n", pkg); return 1; }
|
||
long long sz=dir_size(dirp);
|
||
char hs[24]; human_size(sz,hs,sizeof hs);
|
||
printf("Package: %s\n", pkg);
|
||
printf("Name: %s\n", m.name);
|
||
printf("Version: %s\n", m.version[0]?m.version:"(none)");
|
||
printf("Author: %s\n", m.author[0]?m.author:"(none)");
|
||
printf("Exec: %s\n", m.exec);
|
||
printf("Size: %s\n", hs);
|
||
printf("ExecFile:%s\n", access(m.exec,X_OK)==0?" yes":" no");
|
||
if (m.depends[0]) printf("Depends: %s\n", m.depends);
|
||
if (m.description[0]) printf("Desc: %s\n", m.description);
|
||
if (m.has_routes) printf("Routes: %s\n", m.routes);
|
||
if (m.has_services) printf("Services:%s\n", m.services);
|
||
return 0;
|
||
}
|
||
|
||
static int cmd_remove(const char *pkg, int purge)
|
||
{
|
||
if (!pkg || !valid_pkg(pkg)) { fprintf(stderr,"nyai: remove: 包名无效\n"); return 2; }
|
||
char dst[BUF]; snprintf(dst,sizeof dst,"%s/MeowAPPs/%s", meow_root(), pkg);
|
||
if (access(dst,F_OK)!=0) { fprintf(stderr,"nyai: remove: 未安装 %s\n", pkg); return 1; }
|
||
printf("Removing %s ...\n", pkg);
|
||
rm_rf(dst);
|
||
remove_pkg_backups(pkg); /* 清理 install 升级留下的 .bak_<pid> */
|
||
if (purge) {
|
||
char dd[BUF]; snprintf(dd,sizeof dd,"%s/MeowData/%s", meow_root(), pkg);
|
||
if (access(dd,F_OK)==0) { printf("Purging %s (data) ...\n", pkg); rm_rf(dd); }
|
||
}
|
||
printf("OK\n");
|
||
return 0;
|
||
}
|
||
|
||
static int cmd_export(const char *pkg, const char *out)
|
||
{
|
||
if (!pkg || !valid_pkg(pkg)) { fprintf(stderr,"nyai: export: 包名无效\n"); return 2; }
|
||
char src[BUF],dst[BUF];
|
||
snprintf(src,sizeof src,"%s/MeowAPPs/%s", meow_root(), pkg);
|
||
if (access(src,R_OK)!=0) { fprintf(stderr,"nyai: export: 未安装 %s\n", pkg); return 1; }
|
||
char outbuf[BUF];
|
||
if (out && *out && unsafe_path(out)) { fprintf(stderr,"nyai: export: 输出路径含非法字符\n"); return 1; }
|
||
snprintf(outbuf,sizeof outbuf,"%s", out && *out ? out : pkg);
|
||
if (!strstr(outbuf,".mpk")) strncat(outbuf,".mpk", sizeof outbuf-strlen(outbuf)-1);
|
||
snprintf(dst,sizeof dst,"%s", outbuf);
|
||
printf("Exporting %s -> %s ...\n", pkg, dst);
|
||
char cmd[1024];
|
||
snprintf(cmd,sizeof cmd,"tar -c -C '%s' . 2>/dev/null | gzip -c > '%s.tmp' 2>/dev/null", src, dst);
|
||
system(cmd);
|
||
char tmpp[BUF]; snprintf(tmpp,sizeof tmpp,"%s.tmp", dst);
|
||
if (access(tmpp,F_OK)!=0 || rename(tmpp,dst)!=0) {
|
||
fprintf(stderr,"nyai: export: tar/gzip 失败\n"); rm_rf(tmpp); return 1;
|
||
}
|
||
printf("OK\n");
|
||
return 0;
|
||
}
|
||
|
||
static int cmd_scan(const char *dir)
|
||
{
|
||
char r[BUF]; snprintf(r,sizeof r,"%s", dir && *dir ? dir : ".");
|
||
DIR *d = opendir(r);
|
||
if (!d) { fprintf(stderr,"nyai: scan: 打开 %s 失败\n", r); return 1; }
|
||
printf("Scanning %s ...\n", r);
|
||
printf("%-24s %-10s %-10s %s\n","Package","Version","Size","File");
|
||
struct dirent *de; int found=0;
|
||
while ((de=readdir(d))) {
|
||
if (de->d_name[0]=='.') continue;
|
||
size_t L=strlen(de->d_name);
|
||
if (L<4 || strcmp(de->d_name+L-4,".mpk")!=0) continue;
|
||
char f[BUF]; snprintf(f,sizeof f,"%s/%s", r, de->d_name);
|
||
char stem[128]; snprintf(stem,sizeof stem,"%.*s",(int)(L-4),de->d_name);
|
||
struct stat st; long long sz=0; if (stat(f,&st)==0) sz=st.st_size;
|
||
char hs[24]; human_size(sz,hs,sizeof hs);
|
||
char nm[256]="", ver[64]="";
|
||
appmeta m;
|
||
if (mpk_appinfo(f,&m)==0) { snprintf(nm,sizeof nm,"%s",m.name); snprintf(ver,sizeof ver,"%s",m.version); }
|
||
printf("%-24s %-10s %-10s %s\n", nm[0]?nm:stem, ver[0]?ver:"?", hs, de->d_name);
|
||
found++;
|
||
}
|
||
closedir(d);
|
||
if (!found) printf("No .mpk packages found.\n");
|
||
else printf("\n%d package(s) available.\n", found);
|
||
return 0;
|
||
}
|
||
|
||
static int cmd_upgrade(const char *pkg, const char *file)
|
||
{
|
||
if (!pkg || !valid_pkg(pkg)) { fprintf(stderr,"nyai: upgrade: 包名无效\n"); return 2; }
|
||
char mpk[BUF];
|
||
if (file && *file) snprintf(mpk,sizeof mpk,"%s", file);
|
||
else snprintf(mpk,sizeof mpk,"%s.mpk", pkg);
|
||
if (access(mpk,R_OK)!=0) { fprintf(stderr,"nyai: upgrade: 找不到包文件 %s\n", mpk); return 1; }
|
||
char dst[BUF]; snprintf(dst,sizeof dst,"%s/MeowAPPs/%s", meow_root(), pkg);
|
||
if (access(dst,F_OK)!=0) { fprintf(stderr,"nyai: upgrade: %s 未安装(如需全新安装请用 install)\n", pkg); return 1; }
|
||
char bak[BUF]; snprintf(bak,sizeof bak,"%s.bak_%d", dst,(int)getpid());
|
||
rm_rf(bak);
|
||
if (rename(dst,bak)!=0) { fprintf(stderr,"nyai: upgrade: 备份失败: %s\n", strerror(errno)); return 1; }
|
||
if (mkdir_p(dst)!=0) { rename(bak,dst); return 1; }
|
||
char cmd[1024];
|
||
snprintf(cmd,sizeof cmd,"gzip -dc '%s' | tar -x -C '%s' 2>/dev/null && [ -f '%s/app.json' ]", mpk,dst,dst);
|
||
printf("Upgrading %s ...\n", pkg);
|
||
if (system(cmd)!=0) { fprintf(stderr,"nyai: upgrade: 解压失败,回滚\n"); rm_rf(dst); rename(bak,dst); return 1; }
|
||
char appj[BUF]; snprintf(appj,sizeof appj,"%s/app.json", dst);
|
||
appmeta m;
|
||
if (load_app_meta(appj,&m)!=0) { fprintf(stderr,"nyai: upgrade: 新包 app.json 不合法,回滚\n"); rm_rf(dst); rename(bak,dst); return 1; }
|
||
char d1[BUF],d2[BUF];
|
||
path_data(d1,sizeof d1,pkg,"data"); path_data(d2,sizeof d2,pkg,"cache");
|
||
mkdir_p(d1); mkdir_p(d2);
|
||
printf("OK (%s) — data preserved\n", m.version[0]?m.version:"?");
|
||
return 0;
|
||
}
|
||
|
||
/* ---------------- usage / main ---------------- */
|
||
static void usage(FILE *o, const char *argv0)
|
||
{
|
||
fprintf(o,
|
||
"nyai %s — MeowOS package manager (apt/yum style)\n"
|
||
"\n"
|
||
"Usage:\n"
|
||
" %s update Read all sources into local index cache\n"
|
||
" %s install <pkg|file.mpk> Install by index name or direct .mpk (resolves Depends)\n"
|
||
" %s remove <pkg> [--purge] Uninstall (--purge also deletes MeowData)\n"
|
||
" %s list List installed apps (MeowAPPs/*)\n"
|
||
" %s info <pkg> Show package details (app.json)\n"
|
||
" %s search [pattern] Search index\n"
|
||
" %s list-upgradable Show packages with newer index version\n"
|
||
" %s index <dir> Generate Packages index for a .mpk dir\n"
|
||
" %s export <pkg> [out.mpk] Pack MeowAPPs/<pkg> -> <pkg>.mpk (no data)\n"
|
||
" %s scan [dir] List .mpk in dir (default .)\n"
|
||
" %s upgrade <pkg> [file.mpk] Upgrade app, keep MeowData\n"
|
||
" %s --help This help\n"
|
||
"\n"
|
||
"Sources: %s/MeowOS/opt/nyai/sources.list (local <dir> / http <url> / deb [trusted=yes] <uri>; # comment)\n"
|
||
"Exit codes: 0 success, non-zero error.\n",
|
||
NYAI_VERSION, argv0,argv0,argv0,argv0,argv0,argv0,argv0,argv0,argv0,argv0,argv0,argv0,
|
||
meow_root());
|
||
}
|
||
|
||
int main(int argc, char **argv)
|
||
{
|
||
if (argc<2) { usage(stderr, argv[0]); return 1; }
|
||
const char *cmd = argv[1];
|
||
if (!strcmp(cmd,"--help")||!strcmp(cmd,"-h")) { usage(stdout, argv[0]); return 0; }
|
||
if (!strcmp(cmd,"update")) { return cmd_update(); }
|
||
if (!strcmp(cmd,"install")) {
|
||
if (argc<3) { fprintf(stderr,"nyai: install: 缺包名或 .mpk\n"); return 2; }
|
||
load_sources();
|
||
return cmd_install(argv[2]);
|
||
}
|
||
if (!strcmp(cmd,"remove")) {
|
||
if (argc<3) { fprintf(stderr,"nyai: remove: 缺包名\n"); return 2; }
|
||
int purge=0; for (int i=3;i<argc;i++) if (!strcmp(argv[i],"--purge")) purge=1;
|
||
return cmd_remove(argv[2], purge);
|
||
}
|
||
if (!strcmp(cmd,"list")) return cmd_list();
|
||
if (!strcmp(cmd,"info")) {
|
||
if (argc<3) { fprintf(stderr,"nyai: info: 缺包名\n"); return 2; }
|
||
return cmd_info(argv[2]);
|
||
}
|
||
if (!strcmp(cmd,"search")) { load_sources(); pkgrec recs[MAX_PKG]; int n=load_all_indexes(recs,MAX_PKG,g_srcs,g_nsrc); return cmd_search(argc>2?argv[2]:NULL, recs, n); }
|
||
if (!strcmp(cmd,"list-upgradable")) { load_sources(); pkgrec recs[MAX_PKG]; int n=load_all_indexes(recs,MAX_PKG,g_srcs,g_nsrc); return cmd_list_upgradable(recs,n); }
|
||
if (!strcmp(cmd,"index")) {
|
||
if (argc<3) { fprintf(stderr,"nyai: index: 缺目录\n"); return 2; }
|
||
return cmd_index(argv[2]);
|
||
}
|
||
if (!strcmp(cmd,"export")) {
|
||
if (argc<3) { fprintf(stderr,"nyai: export: 缺包名\n"); return 2; }
|
||
return cmd_export(argv[2], argc>3?argv[3]:NULL);
|
||
}
|
||
if (!strcmp(cmd,"scan")) return cmd_scan(argc>2?argv[2]:NULL);
|
||
if (!strcmp(cmd,"upgrade")) {
|
||
if (argc<3) { fprintf(stderr,"nyai: upgrade: 缺包名\n"); return 2; }
|
||
return cmd_upgrade(argv[2], argc>3?argv[3]:NULL);
|
||
}
|
||
fprintf(stderr,"nyai: 未知命令 '%s'(--help 查看用法)\n", cmd);
|
||
return 2;
|
||
}
|