feat(etapa1): baixar e empacotar sidecar sherpa-onnx-offline

This commit is contained in:
Evandro committed 2026-06-11 02:48:51 -03:00
1 parent b2554006a4
commit c5baf7a23f
11 files changed
+417 -7

No files matched your search

+167
View File
@@ -0,0 +1,167 @@
// Baixa o release `*-shared` oficial do k2-fsa/sherpa-onnx v1.12.23 e extrai
// apenas o binário `sherpa-onnx-offline` para `src-tauri/binaries/sherpa-onnx-offline-<triple><ext>`.
//
// O shared lib já vem com as DLLs/so de `onnxruntime` no diretório `bin/`. Essas
// DLLs adicionais são empacotadas como `resources` do Tauri (ver tauri.conf.json)
// para que o executável as encontre em produção.
//
// Uso:
// node scripts/setup-sidecar.mjs
//
// Variáveis de ambiente opcionais:
// SHERPA_ONNX_VERSION (default: v1.12.23)
// SHERPA_ONNX_TRIPLE (default: detectado via `rustc -vV`)
import { execFileSync } from "node:child_process";
import {
existsSync,
mkdirSync,
chmodSync,
createWriteStream,
statSync,
renameSync,
copyFileSync,
} from "node:fs";
import { dirname, join, resolve, sep as pathSep } from "node:path";
import { pipeline } from "node:stream/promises";
import { fileURLToPath } from "node:url";
import process from "node:process";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const repoRoot = resolve(__dirname, "..");
const binariesDir = join(repoRoot, "src-tauri", "binaries");
mkdirSync(binariesDir, { recursive: true });
const DEFAULT_VERSION = "v1.12.23";
const PLATFORM_MAP = [
{ keys: ["linux", "x64"], asset: "sherpa-onnx-v1.12.23-linux-x64-shared.tar.bz2", triple: "x86_64-unknown-linux-gnu", ext: "" },
{ keys: ["linux", "arm64"], asset: "sherpa-onnx-v1.12.23-linux-aarch64-shared.tar.bz2", triple: "aarch64-unknown-linux-gnu", ext: "" },
{ keys: ["darwin", "x64"], asset: "sherpa-onnx-v1.12.23-osx-universal2-shared.tar.bz2", triple: "x86_64-apple-darwin", ext: "" },
{ keys: ["darwin", "arm64"], asset: "sherpa-onnx-v1.12.23-osx-universal2-shared.tar.bz2", triple: "aarch64-apple-darwin", ext: "" },
{ keys: ["win32", "x64"], asset: "sherpa-onnx-v1.12.23-win-x64-shared.tar.bz2", triple: "x86_64-pc-windows-msvc", ext: ".exe" },
];
const RELEASE_BASE = "https://github.com/k2-fsa/sherpa-onnx/releases/download";
function detectTriple() {
if (process.env.SHERPA_ONNX_TRIPLE) return process.env.SHERPA_ONNX_TRIPLE;
try {
const output = execFileSync("rustc", ["-vV"], { encoding: "utf8" });
const line = output.split("\n").find((l) => l.startsWith("host:"));
if (line) return line.replace("host:", "").trim();
} catch {
// cai para detecção por platform/arch
}
const os = process.platform;
const archName = process.arch;
return PLATFORM_MAP.find(
(p) => (p.keys[0] === "win32" ? os === "win32" : p.keys[0] === os) && p.keys[1] === archName,
)?.triple;
}
function pickAsset(triple) {
const found = PLATFORM_MAP.find((p) => p.triple === triple);
if (!found) {
throw new Error(
`Não há binário pré-buildado para o target ${triple}. Edite scripts/setup-sidecar.mjs para apontar para um build customizado.`,
);
}
return found;
}
async function download(url, dest) {
const res = await fetch(url);
if (!res.ok) {
throw new Error(`download falhou: ${res.status} ${res.statusText} em ${url}`);
}
if (!res.body) {
throw new Error(`resposta vazia em ${url}`);
}
await pipeline(res.body, createWriteStream(dest));
return statSync(dest).size;
}
function extractTarBzip2(archive, cwd) {
// `tar` (bsdtar) no Windows não aceita bem paths com backslash nem `D:`
// em `-C`. Usamos `cwd` + path relativo para evitar isso.
const prefix = cwd.endsWith(pathSep) ? cwd : cwd + pathSep;
const relArchive = archive.startsWith(prefix) ? archive.slice(prefix.length) : archive;
execFileSync("tar", ["-xjf", relArchive], { stdio: "inherit", cwd });
}
function moveExtractedBin(extractedBinDir, expectedNames, dest) {
for (const name of expectedNames) {
const candidate = join(extractedBinDir, name);
if (existsSync(candidate)) {
renameSync(candidate, dest);
return;
}
}
throw new Error(
`binário sherpa-onnx-offline não encontrado em ${extractedBinDir}; nomes esperados: ${expectedNames.join(", ")}`,
);
}
function copyExtraSharedLibs(extractedRoot, ext) {
// As DLLs adicionais (onnxruntime, providers_shared) precisam acompanhar o
// executável em runtime. Copiamos para `src-tauri/binaries/` lado a lado
// do binário renomeado; o empacotamento final via tauri.conf.json
// resources cuida de movê-las para junto do .exe no instalador.
const binDir = join(extractedRoot, "bin");
if (!existsSync(binDir)) return;
for (const file of ["onnxruntime.dll", "onnxruntime_providers_shared.dll", "cargs.dll"]) {
const src = join(binDir, file);
if (existsSync(src)) {
const destName = ext === ".exe" ? file : file.replace(/\.dll$/, ".so");
copyFileSync(src, join(binariesDir, destName));
}
}
}
async function main() {
const triple = detectTriple();
if (!triple) {
throw new Error("Não foi possível detectar o target triple do host.");
}
const asset = pickAsset(triple);
const version = process.env.SHERPA_ONNX_VERSION || DEFAULT_VERSION;
const assetName = asset.asset.replace(/v1\.12\.23/, version);
const versionNoV = version.replace(/^v/, "");
const url = `${RELEASE_BASE}/v${versionNoV}/${assetName}`;
const finalPath = join(binariesDir, `sherpa-onnx-offline-${triple}${asset.ext}`);
if (existsSync(finalPath) && statSync(finalPath).size > 1024 * 1024) {
console.log(`✓ sidecar já presente em ${finalPath} (${statSync(finalPath).size} bytes)`);
return;
}
const archivePath = join(binariesDir, assetName);
console.log(`↓ baixando ${url}`);
const size = await download(url, archivePath);
console.log(`✓ ${size} bytes baixados em ${archivePath}`);
console.log("↓ extraindo…");
extractTarBzip2(archivePath, binariesDir);
const extractedRoot = join(binariesDir, assetName.replace(/\.tar\.bz2$/, ""));
const expected = asset.ext === ".exe" ? ["sherpa-onnx-offline.exe", "sherpa-onnx-offline"] : ["sherpa-onnx-offline"];
moveExtractedBin(join(extractedRoot, "bin"), expected, finalPath);
copyExtraSharedLibs(extractedRoot, asset.ext);
if (asset.ext === "") {
try {
chmodSync(finalPath, 0o755);
} catch {
// ignore em Windows
}
}
console.log(`✓ sidecar pronto em ${finalPath}`);
}
main().catch((error) => {
console.error(`falha: ${error.message}`);
process.exitCode = 1;
});
+53
View File
@@ -0,0 +1,53 @@
import { describe, it, expect, beforeAll } from "vitest";
import { execFileSync } from "node:child_process";
import { existsSync, statSync, rmSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const repoRoot = join(__dirname, "..");
const SUPPORTED_HOST = "x86_64-pc-windows-msvc";
function detectHostTriple() {
try {
const output = execFileSync("rustc", ["-vV"], { encoding: "utf8" });
const line = output.split("\n").find((l) => l.startsWith("host:"));
return line ? line.replace("host:", "").trim() : null;
} catch {
return null;
}
}
describe("setup-sidecar", () => {
const hostTriple = detectHostTriple();
beforeAll(() => {
if (!hostTriple) {
console.warn("rustc não disponível; pulando teste de sidecar");
}
});
it("baixa o binário oficial e o coloca em src-tauri/binaries com o target triple", async () => {
if (!hostTriple) return;
if (hostTriple !== SUPPORTED_HOST) {
console.log(`host triple ${hostTriple} fora da matriz de testes; pulando`);
return;
}
const binaryPath = join(repoRoot, "src-tauri", "binaries", `sherpa-onnx-offline-${hostTriple}.exe`);
if (existsSync(binaryPath)) {
rmSync(binaryPath, { force: true });
}
execFileSync("node", [join(repoRoot, "scripts", "setup-sidecar.mjs")], {
stdio: "pipe",
env: { ...process.env, SHERPA_ONNX_VERSION: "v1.12.23" },
});
expect(existsSync(binaryPath)).toBe(true);
expect(statSync(binaryPath).size).toBeGreaterThan(1024 * 1024);
}, 180_000);
});