fix(models): Assimp 缺失时给出可操作报错,部署脚本自动安装
现象:服务器未安装 Assimp,上传 PMX 压缩包只得到 GLB 直出:导出失败(ENOENT):spawn assimp ENOENT glTF 直出:导出失败(ENOENT):spawn assimp ENOENT 用户看不出缺什么、怎么装,也不知道只传 glb/vrm 其实不需要它。 修复: 1. 新增 src/common/assimp.ts:按 ASSIMP_BIN -> PATH -> /usr/bin、/usr/local/bin 等常见目录定位可执行文件(只认文件,不认同名目录),转换前先预检; 找不到就一次性给出各发行版安装命令、查过的路径与替代办法, 不再打两条 ENOENT。动作预处理(FBX/glTF -> BVH)复用同一文案。 2. 新增 deploy/assimp-lib.sh:install.sh 按发行版自动安装 (apt-get / dnf / yum / zypper / apk / pacman),装在非常规目录时把绝对 路径写入 deploy/.env 的 ASSIMP_BIN;装不上只告警不中止,可用 SKIP_ASSIMP_INSTALL=1 跳过。check-access.sh 自检增加 Assimp 一行。 3. /health 增加 assimp.path;管理端「概览 -> 服务健康」显示 Assimp 状态, 不可用时给出安装提示。 4. 新增 test/assimp-locate.spec.ts 13 项;强化「assimp 不存在」用例断言安装命令。 测试规模 12 文件 / 193 项。 5. 版本 0.2.2,README(.md/.txt)/DSH.md 同步(前置要求、FAQ、环境变量说明)。
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "opendance-admin",
|
||||
"private": true,
|
||||
"version": "0.2.1",
|
||||
"version": "0.2.2",
|
||||
"type": "module",
|
||||
"description": "OpenDance Studio 管理端(React + TypeScript + Vite)",
|
||||
"scripts": {
|
||||
|
||||
@@ -273,12 +273,21 @@ export interface HealthDatabase {
|
||||
migrationsPending?: number;
|
||||
}
|
||||
|
||||
export interface HealthAssimp {
|
||||
available: boolean;
|
||||
/** 解析到的 Assimp 可执行文件路径 */
|
||||
path?: string;
|
||||
version?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface Health {
|
||||
status: 'ok' | 'degraded';
|
||||
uptimeSec: number;
|
||||
version: string;
|
||||
contractVersion: Record<string, string>;
|
||||
database: HealthDatabase;
|
||||
assimp?: HealthAssimp;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ 错误响应 */
|
||||
|
||||
@@ -196,10 +196,28 @@ export function DashboardPage() {
|
||||
<span className="muted">迁移待执行</span>
|
||||
<code>{healthValue.database?.migrationsPending ?? 0}</code>
|
||||
</div>
|
||||
<div className="kv">
|
||||
<span className="muted">assimp</span>
|
||||
<code>
|
||||
{healthValue.assimp?.available
|
||||
? (healthValue.assimp.version ?? 'available')
|
||||
: '未安装'}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<ErrorBox message={health.error ?? '无法获取健康状态'} onRetry={state.reload} />
|
||||
)}
|
||||
{health.ok && healthValue && !healthValue.assimp?.available ? (
|
||||
<div className="banner banner-warn" role="status">
|
||||
<strong>Assimp 不可用</strong>
|
||||
<span>
|
||||
上传 .pmx / .fbx 模型(或含 PMX/FBX 的压缩包)会转换失败;FBX / glTF 动作预处理同样
|
||||
需要它。在服务器上安装后无需重启服务:<code>apt-get install -y assimp-utils</code>
|
||||
(RHEL 系为 <code>dnf install -y assimp</code>)。只上传 .glb / .vrm 则不受影响。
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "opendance-web-server",
|
||||
"version": "0.2.1",
|
||||
"version": "0.2.2",
|
||||
"description": "OpenDance Studio Web 管理服务端(NestJS + TypeORM + 现有 MySQL 实例)",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
/**
|
||||
* Assimp 可执行文件的定位、安装提示与可用性探测。
|
||||
*
|
||||
* 背景:PMX / FBX 模型转换与 FBX / glTF 动作预处理都依赖**外部** Assimp
|
||||
* 命令行工具(BSD-3-Clause,不在 Node 依赖里)。装漏了只有在用户上传完
|
||||
* 一整个压缩包之后才会暴露,而且原始报错只有 `spawn assimp ENOENT`。
|
||||
* 这里把「找二进制」与「怎么装」集中到一处,供转换管线、动作预处理、
|
||||
* /health 与部署自检共用。
|
||||
*/
|
||||
import { execFile } from 'node:child_process';
|
||||
import { existsSync, statSync } from 'node:fs';
|
||||
import { delimiter, join } from 'node:path';
|
||||
import { promisify } from 'node:util';
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
/**
|
||||
* 除 PATH 之外再查找的常见目录。
|
||||
* 生产上服务由系统服务托管,PATH 可能很窄(甚至只有 /usr/bin),
|
||||
* 而用户把 Assimp 装在 /usr/local/bin 也很常见。
|
||||
*/
|
||||
export const ASSIMP_FALLBACK_DIRS = [
|
||||
'/usr/bin',
|
||||
'/usr/local/bin',
|
||||
'/usr/sbin',
|
||||
'/usr/local/sbin',
|
||||
'/snap/bin',
|
||||
];
|
||||
|
||||
export interface AssimpLocation {
|
||||
/** 解析到的可执行文件路径(未找到时原样返回传入的 bin) */
|
||||
path: string;
|
||||
found: boolean;
|
||||
/** 依次查找过的候选路径(用于错误信息) */
|
||||
searched: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 定位 Assimp 可执行文件:显式路径 -> PATH -> 常见系统目录。
|
||||
* 只做文件系统检查,不执行它(探测可用性见 detectAssimpBin)。
|
||||
*/
|
||||
export function resolveAssimpBinary(bin: string): AssimpLocation {
|
||||
const candidates: string[] = [];
|
||||
if (bin.includes('/') || bin.includes('\\')) {
|
||||
candidates.push(bin);
|
||||
} else {
|
||||
for (const dir of (process.env.PATH ?? '').split(delimiter)) {
|
||||
if (dir) candidates.push(join(dir, bin));
|
||||
}
|
||||
for (const dir of ASSIMP_FALLBACK_DIRS) candidates.push(join(dir, bin));
|
||||
}
|
||||
const searched = [...new Set(candidates)];
|
||||
for (const candidate of searched) {
|
||||
try {
|
||||
// 必须是文件:只判 existsSync 会把同名目录(例如 ASSIMP_BIN=/usr/bin)当成二进制
|
||||
if (existsSync(candidate) && statSync(candidate).isFile()) {
|
||||
return { path: candidate, found: true, searched };
|
||||
}
|
||||
} catch {
|
||||
/* 权限等问题一律视为该候选不可用,继续找下一个 */
|
||||
}
|
||||
}
|
||||
return { path: bin, found: false, searched };
|
||||
}
|
||||
|
||||
/** 各发行版的安装命令(部署脚本与错误信息共用同一份文案) */
|
||||
export const ASSIMP_INSTALL_COMMANDS: ReadonlyArray<{ distro: string; command: string }> = [
|
||||
{ distro: 'Debian / Ubuntu', command: 'apt-get update && apt-get install -y assimp-utils libassimp6' },
|
||||
{ distro: 'RHEL / CentOS / Rocky / Fedora', command: 'dnf install -y assimp' },
|
||||
{ distro: 'Alpine', command: 'apk add --no-cache assimp' },
|
||||
{ distro: 'Arch', command: 'pacman -S --noconfirm assimp' },
|
||||
];
|
||||
|
||||
export function assimpInstallHint(indent = ' '): string {
|
||||
return ASSIMP_INSTALL_COMMANDS.map((item) => `${indent}${item.distro}: ${item.command}`).join('\n');
|
||||
}
|
||||
|
||||
/** 找不到 Assimp 时的完整可读错误(含查过的路径、安装命令与替代办法) */
|
||||
export function assimpMissingMessage(bin: string, searched: string[] = []): string {
|
||||
const tried =
|
||||
searched.length > 0
|
||||
? `\n已查找 ${searched.length} 处: ${searched.slice(0, 5).join(', ')}${searched.length > 5 ? ' ...' : ''}`
|
||||
: '';
|
||||
return (
|
||||
`未找到 Assimp 可执行文件 "${bin}"。PMX / FBX 模型转换与 FBX / glTF 动作预处理都需要它` +
|
||||
'(外部命令行工具, 不在 Node 依赖内)。' +
|
||||
tried +
|
||||
'\n安装方式:\n' +
|
||||
assimpInstallHint() +
|
||||
'\n装好后可用 `assimp version` 验证; 若它不在服务进程的 PATH 里, ' +
|
||||
'请在 deploy/.env 中把 ASSIMP_BIN 设为绝对路径并重启服务。' +
|
||||
'\n只上传 .glb / .vrm 模型(或 .bvh / .vmd / 关键点 JSON 动作)则不需要 Assimp。'
|
||||
);
|
||||
}
|
||||
|
||||
export interface AssimpProbe {
|
||||
available: boolean;
|
||||
/** 解析到的路径(未找到时为原值) */
|
||||
path: string;
|
||||
version?: string;
|
||||
error?: string;
|
||||
searched: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 探测 Assimp 是否真的可用(执行 `assimp version`)。
|
||||
* 用于 /health、部署自检,以及转换前的预检。
|
||||
*/
|
||||
export async function detectAssimpBin(bin: string): Promise<AssimpProbe> {
|
||||
const location = resolveAssimpBinary(bin);
|
||||
if (!location.found) {
|
||||
return {
|
||||
available: false,
|
||||
path: location.path,
|
||||
error: `未安装 (${bin})`,
|
||||
searched: location.searched,
|
||||
};
|
||||
}
|
||||
try {
|
||||
const { stdout } = await execFileAsync(location.path, ['version'], { timeout: 10_000 });
|
||||
const version =
|
||||
stdout.split(/\r?\n/).find((line) => line.toLowerCase().includes('assimp')) ?? stdout.trim();
|
||||
return { available: true, path: location.path, version: version.trim(), searched: location.searched };
|
||||
} catch (error) {
|
||||
const err = error as NodeJS.ErrnoException;
|
||||
return {
|
||||
available: false,
|
||||
path: location.path,
|
||||
error: err.code === 'ENOENT' ? `未安装 (${bin})` : (err.message ?? '未知错误'),
|
||||
searched: location.searched,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,8 @@ export interface HealthReport {
|
||||
};
|
||||
assimp: {
|
||||
available: boolean;
|
||||
/** 解析到的 Assimp 可执行文件路径(未找到时为配置值) */
|
||||
path?: string;
|
||||
version?: string;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@ import { promisify } from 'node:util';
|
||||
import { ModelFormat } from '../common/enums';
|
||||
import { ZipEntry, readZipEntries } from '../common/zip-reader';
|
||||
import { embedGlbImages } from './glb-textures';
|
||||
import { assimpMissingMessage, resolveAssimpBinary } from '../common/assimp';
|
||||
import { packGltfToGlb } from './gltf-packer';
|
||||
import { VrmWrapMeta, detectGlbKind, wrapGlbAsVrm } from './vrm-writer';
|
||||
|
||||
@@ -250,6 +251,13 @@ async function runAssimpPipeline(
|
||||
): Promise<{ buffer: Buffer; format: ModelFormat; note: string; attempts: string[] }> {
|
||||
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
||||
const prefer = options.preferFormat ?? 'glb';
|
||||
|
||||
// 预检:Assimp 缺失时直接给出「怎么装 + 查过哪些路径」,而不是两条 spawn ENOENT。
|
||||
// 只做文件系统检查(不执行),避免给假二进制(测试桩)增加额外要求。
|
||||
const location = resolveAssimpBinary(options.assimpBin);
|
||||
if (!location.found) {
|
||||
throw new Error(`模型转换失败:${assimpMissingMessage(options.assimpBin, location.searched)}`);
|
||||
}
|
||||
{
|
||||
const workDir = dirname(modelPath);
|
||||
const attempts: string[] = [];
|
||||
|
||||
@@ -4,6 +4,8 @@ import { existsSync } from 'node:fs';
|
||||
import { basename, extname, join } from 'node:path';
|
||||
import { promisify } from 'node:util';
|
||||
|
||||
import { assimpMissingMessage, resolveAssimpBinary } from '../common/assimp';
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
export const ASSIMP_CONVERT_TIMEOUT_MS = 120_000;
|
||||
@@ -31,10 +33,8 @@ export async function convertToBvhWithAssimp(
|
||||
} catch (error) {
|
||||
const err = error as NodeJS.ErrnoException & { stderr?: string };
|
||||
if (err.code === 'ENOENT') {
|
||||
throw new BadRequestException(
|
||||
`未找到 Assimp 可执行文件「${assimpBin}」。FBX / glTF 动作预处理需要 Assimp(BSD-3-Clause),` +
|
||||
'请安装后设置 ASSIMP_BIN 环境变量;BVH 动作无需 Assimp。',
|
||||
);
|
||||
const location = resolveAssimpBinary(assimpBin);
|
||||
throw new BadRequestException(assimpMissingMessage(assimpBin, location.searched));
|
||||
}
|
||||
const detail = (err.stderr ?? err.message ?? '').toString().trim().slice(0, 500);
|
||||
throw new BadRequestException(`Assimp 转换失败:${detail || '未知错误'}`);
|
||||
@@ -47,16 +47,4 @@ export async function convertToBvhWithAssimp(
|
||||
}
|
||||
|
||||
/** 探测 Assimp 是否可用(用于 /health 与部署自检) */
|
||||
export async function detectAssimp(assimpBin: string): Promise<{ available: boolean; version?: string; error?: string }> {
|
||||
try {
|
||||
const { stdout } = await execFileAsync(assimpBin, ['version'], { timeout: 10_000 });
|
||||
const version = stdout.split(/\r?\n/).find((line) => line.toLowerCase().includes('assimp')) ?? stdout.trim();
|
||||
return { available: true, version: version.trim() };
|
||||
} catch (error) {
|
||||
const err = error as NodeJS.ErrnoException;
|
||||
return {
|
||||
available: false,
|
||||
error: err.code === 'ENOENT' ? `未安装(${assimpBin})` : (err.message ?? '未知错误'),
|
||||
};
|
||||
}
|
||||
}
|
||||
export { detectAssimpBin as detectAssimp } from '../common/assimp';
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join, resolve } from 'node:path';
|
||||
import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
ASSIMP_FALLBACK_DIRS,
|
||||
assimpInstallHint,
|
||||
assimpMissingMessage,
|
||||
detectAssimpBin,
|
||||
resolveAssimpBinary,
|
||||
} from '../src/common/assimp';
|
||||
import { convertModelFile } from '../src/models/model-convert';
|
||||
|
||||
/**
|
||||
* Assimp 缺失时的可诊断性。
|
||||
*
|
||||
* 背景(真实故障):服务器没装 Assimp,上传 PMX 压缩包只得到
|
||||
* GLB 直出:导出失败(ENOENT):spawn assimp ENOENT
|
||||
* glTF 直出:导出失败(ENOENT):spawn assimp ENOENT
|
||||
* 用户看不出「缺什么、怎么装」。现在:
|
||||
* * 转换前先定位二进制,找不到就一次性给出安装命令与查过的路径;
|
||||
* * 定位逻辑覆盖 PATH 之外的常见目录(服务由 systemd 托管时 PATH 可能很窄)。
|
||||
*/
|
||||
|
||||
let workRoot = '';
|
||||
let fakeBin = '';
|
||||
|
||||
function findDeployLib(): string {
|
||||
const candidates = [
|
||||
resolve(process.cwd(), '..', 'deploy', 'assimp-lib.sh'),
|
||||
resolve(process.cwd(), 'deploy', 'assimp-lib.sh'),
|
||||
resolve(process.cwd(), '..', '..', 'deploy', 'assimp-lib.sh'),
|
||||
];
|
||||
for (const candidate of candidates) {
|
||||
if (existsSync(candidate)) return candidate;
|
||||
}
|
||||
throw new Error(`找不到 deploy/assimp-lib.sh,候选路径:\n${candidates.join('\n')}`);
|
||||
}
|
||||
|
||||
const ASSIMP_LIB = findDeployLib();
|
||||
|
||||
/** 写一个最小的假 assimp:只实现 `version` */
|
||||
function writeFakeAssimp(path: string, versionLine = 'Assimp version 6.0.4'): string {
|
||||
mkdirSync(join(path, '..'), { recursive: true });
|
||||
writeFileSync(path, `#!/bin/bash\n[ "\${1:-}" = "version" ] && echo "${versionLine}"\nexit 0\n`);
|
||||
chmodSync(path, 0o755);
|
||||
return path;
|
||||
}
|
||||
|
||||
beforeAll(() => {
|
||||
workRoot = mkdtempSync(join(tmpdir(), 'opendance-assimp-test-'));
|
||||
fakeBin = writeFakeAssimp(join(workRoot, 'bin', 'assimp'));
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
rmSync(workRoot, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
const savedPath = process.env.PATH;
|
||||
afterEach(() => {
|
||||
process.env.PATH = savedPath;
|
||||
});
|
||||
|
||||
describe('Assimp 定位(resolveAssimpBinary)', () => {
|
||||
it('能在 PATH 中找到可执行文件', () => {
|
||||
process.env.PATH = `${join(workRoot, 'bin')}:/usr/bin:/bin`;
|
||||
const result = resolveAssimpBinary('assimp');
|
||||
expect(result.found).toBe(true);
|
||||
expect(result.path).toBe(fakeBin);
|
||||
});
|
||||
|
||||
it('显式绝对路径优先,不存在时 found=false 并给出查找记录', () => {
|
||||
const missing = join(workRoot, 'no-such-assimp');
|
||||
const result = resolveAssimpBinary(missing);
|
||||
expect(result.found).toBe(false);
|
||||
expect(result.searched).toContain(missing);
|
||||
|
||||
const present = resolveAssimpBinary(fakeBin);
|
||||
expect(present.found).toBe(true);
|
||||
expect(present.path).toBe(fakeBin);
|
||||
});
|
||||
|
||||
it('PATH 里没有时仍会查常见系统目录(systemd 服务 PATH 可能很窄)', () => {
|
||||
process.env.PATH = '/nonexistent-dir-only';
|
||||
const result = resolveAssimpBinary('assimp');
|
||||
// 结果取决于本机是否装了真实 assimp:两种都必须把常见目录查过
|
||||
for (const dir of ASSIMP_FALLBACK_DIRS) {
|
||||
expect(result.searched).toContain(join(dir, 'assimp'));
|
||||
}
|
||||
if (!result.found) expect(result.path).toBe('assimp');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Assimp 缺失时的错误信息', () => {
|
||||
it('包含二进制名、各发行版安装命令、替代办法与查过的路径', () => {
|
||||
const message = assimpMissingMessage('assimp', ['/usr/bin/assimp', '/usr/local/bin/assimp']);
|
||||
expect(message).toContain('assimp');
|
||||
expect(message).toContain('apt-get install -y assimp-utils');
|
||||
expect(message).toContain('dnf install -y assimp');
|
||||
expect(message).toContain('/usr/bin/assimp');
|
||||
expect(message).toContain('ASSIMP_BIN');
|
||||
expect(message).toContain('.glb / .vrm');
|
||||
});
|
||||
|
||||
it('安装提示各发行版一条命令', () => {
|
||||
const hint = assimpInstallHint();
|
||||
for (const distro of ['Debian / Ubuntu', 'Alpine', 'Arch']) {
|
||||
expect(hint).toContain(distro);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Assimp 可用性探测(detectAssimpBin)', () => {
|
||||
it('能执行时返回版本与路径', async () => {
|
||||
const probe = await detectAssimpBin(fakeBin);
|
||||
expect(probe.available).toBe(true);
|
||||
expect(probe.path).toBe(fakeBin);
|
||||
expect(probe.version).toContain('6.0.4');
|
||||
});
|
||||
|
||||
it('找不到时 available=false 且 error 说明未安装', async () => {
|
||||
const probe = await detectAssimpBin(join(workRoot, 'no-such-assimp'));
|
||||
expect(probe.available).toBe(false);
|
||||
expect(probe.error).toContain('未安装');
|
||||
expect(probe.searched.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('转换管线:Assimp 缺失时快速失败并给出可操作信息', () => {
|
||||
it('convertModelFile 抛出的错误包含安装命令,且不再出现两次 spawn ENOENT', async () => {
|
||||
let message = '';
|
||||
try {
|
||||
await convertModelFile(Buffer.from('PMX dummy'), 'Hatsune Miku.pmx', {
|
||||
assimpBin: join(workRoot, 'no-such-assimp'),
|
||||
timeoutMs: 5_000,
|
||||
});
|
||||
throw new Error('应当抛出异常');
|
||||
} catch (error) {
|
||||
message = error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
expect(message).toContain('模型转换失败');
|
||||
expect(message).toContain('apt-get install -y assimp-utils');
|
||||
expect(message).toContain('ASSIMP_BIN');
|
||||
expect(message).not.toContain('ENOENT');
|
||||
});
|
||||
});
|
||||
|
||||
describe('deploy/assimp-lib.sh:部署脚本共用的安装逻辑', () => {
|
||||
function runLib(script: string, path?: string) {
|
||||
// 覆盖 PATH 的用例里必须用绝对路径调 bash,否则连 bash 都找不到
|
||||
return spawnSync('/bin/bash', ['-c', `. "$1"; ${script}`, '_', ASSIMP_LIB], {
|
||||
encoding: 'utf8',
|
||||
timeout: 15000,
|
||||
env: { ...process.env, ...(path ? { PATH: path } : {}) },
|
||||
});
|
||||
}
|
||||
|
||||
it('opendance_assimp_resolve 能在 PATH 中找到二进制', () => {
|
||||
const result = runLib('opendance_assimp_resolve', `${join(workRoot, 'bin')}:/usr/bin:/bin`);
|
||||
expect(result.stdout.trim()).toBe(fakeBin);
|
||||
expect(result.status).toBe(0);
|
||||
});
|
||||
|
||||
it('opendance_assimp_resolve 在找不到时返回非 0 且无输出', () => {
|
||||
const result = runLib('opendance_assimp_resolve || echo MISSING', '/nonexistent-dir-only');
|
||||
// 本机若真的装了 assimp(/usr/bin),则跳过这条断言的严格性
|
||||
if (!existsSync('/usr/bin/assimp') && !existsSync('/usr/local/bin/assimp')) {
|
||||
expect(result.stdout.trim()).toBe('MISSING');
|
||||
}
|
||||
expect(result.status).toBe(0);
|
||||
});
|
||||
|
||||
it('opendance_assimp_install_cmd 按包管理器给出对应命令', () => {
|
||||
const aptDir = join(workRoot, 'apt');
|
||||
mkdirSync(aptDir, { recursive: true });
|
||||
writeFileSync(join(aptDir, 'apt-get'), '#!/bin/bash\nexit 0\n');
|
||||
chmodSync(join(aptDir, 'apt-get'), 0o755);
|
||||
const result = runLib('opendance_assimp_install_cmd', `${aptDir}:/usr/bin:/bin`);
|
||||
expect(result.stdout).toContain('apt-get install -y assimp-utils libassimp6');
|
||||
});
|
||||
|
||||
it('opendance_assimp_install 调用包管理器安装 assimp-utils(离线失败也不抛)', () => {
|
||||
const aptDir = join(workRoot, 'apt');
|
||||
const log = join(aptDir, 'calls.log');
|
||||
rmSync(log, { force: true });
|
||||
writeFileSync(join(aptDir, 'apt-get'), `#!/bin/bash\necho "apt-get $*" >> "${log}"\nexit 0\n`);
|
||||
chmodSync(join(aptDir, 'apt-get'), 0o755);
|
||||
const result = runLib('opendance_assimp_install', `${aptDir}:/usr/bin:/bin`);
|
||||
expect(result.status).toBe(0);
|
||||
const calls = existsSync(log) ? readFileSync(log, 'utf8') : '';
|
||||
expect(calls).toContain('apt-get install -y -qq assimp-utils libassimp6');
|
||||
});
|
||||
|
||||
it('非常规目录需要写 ASSIMP_BIN,标准目录不需要', () => {
|
||||
expect(runLib(`opendance_assimp_needs_env_path ${join(workRoot, 'bin', 'assimp')} && echo YES`).stdout).toContain('YES');
|
||||
expect(runLib('opendance_assimp_needs_env_path /usr/bin/assimp || echo NO').stdout).toContain('NO');
|
||||
});
|
||||
});
|
||||
@@ -423,13 +423,18 @@ describe('压缩包导入与转换(用假 Assimp 做对照)', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('assimp 不存在时给出可读错误', async () => {
|
||||
it('assimp 不存在时给出可读错误(含安装命令与替代办法,而不是 spawn ENOENT)', async () => {
|
||||
const promise = convertModelArchive(pmxArchive(), {
|
||||
assimpBin: join(workRoot, 'no-such-assimp'),
|
||||
timeoutMs: 5_000,
|
||||
});
|
||||
await expect(promise).rejects.toThrow(/模型转换失败/);
|
||||
await expect(
|
||||
convertModelArchive(pmxArchive(), {
|
||||
assimpBin: join(workRoot, 'no-such-assimp'),
|
||||
timeoutMs: 5_000,
|
||||
}),
|
||||
).rejects.toThrow(/模型转换失败/);
|
||||
).rejects.toThrow(/apt-get install -y assimp-utils[\s\S]*\.glb \/ \.vrm/);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user