Files
MeowOS-For-R3P2/patch_v2/meowpkg/nyai.c
T
MeowOS Dev 245bee2946 MeowOS for HiBy R3 Pro II — 公共发布版本(历史已重置)
• 全新单提交:清除含敏感信息的旧历史(sudo 口令、内网 IP、SSH 密钥路径、热点 SSID、设备 MAC)
• 本地开发文档/测试记录/构建脚本全部移入 gitignored 的 devdocs/ 与 scripts/build/(不入库)
• 保留:MeowOS 源码(launcher/watchdog/bootmenu/meow_svcd/SDK)、项目规范文档、mips-repo
增补组件:SDK 统一日志+通知 API、meow_svcd 服务管理器、mips-repo 软件仓库
2026-08-30 01:12:34 +08:00

1015 lines
39 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 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;
}