Files
desktop/ci-scripts/download-side-binaries.ts
zeekayandHanzo Dev b8f2afd4a5 ci(release): pnpm install + hanzo-node v1.15.0 sidecars + arm64 (1.1.44)
Three fixes to green the Linux legs: (1) npm ci -> pnpm install --frozen-lockfile — this repo is pnpm-native; npm's flat node_modules resolves @hanzo/ai's subpath exports differently and the vite build died with 'Missing "./desktop" specifier in "@hanzo/ai"' (verified the vite build completes under pnpm). (2) HANZO_NODE_VERSION v1.1.20 -> v1.15.0: v1.1.20 was 404 for every arch (stale), v1.15.0 ships hanzo-node + deno + uv sidecars for all targets. (3) download-side-binaries.ts: add aarch64-unknown-linux-gnu to the arch enum + an ollama-linux-arm64 downloader (arm64 crashed at module load on the missing enum value).

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-09 15:40:09 -07:00

294 lines
9.3 KiB
TypeScript
Executable File

import { exec } from 'child_process';
import { createWriteStream } from 'fs';
import { copyFile, cp, mkdir, readdir, rename } from 'fs/promises';
import path from 'path';
import axios from 'axios';
import { ensureFile } from 'fs-extra';
import { extract } from 'tar';
import * as zl from 'zip-lib';
import { z } from 'zod';
enum Arch {
x86_64_unknown_linux_gnu = 'x86_64-unknown-linux-gnu',
aarch64_unknown_linux_gnu = 'aarch64-unknown-linux-gnu',
aarch64_apple_darwin = 'aarch64-apple-darwin',
x86_64_pc_windows_msvc = 'x86_64-pc-windows-msvc',
}
const envSchema = z.object({
ARCH: z.nativeEnum(Arch),
OLLAMA_VERSION: z.string().min(6),
HANZO_NODE_VERSION: z.string().min(6),
});
type Env = z.infer<typeof envSchema>;
const env: Env = envSchema.parse(process.env);
const TEMP_PATH = './temp';
const OLLAMA_RESOURCES_PATH =
'./apps/hanzo-desktop/src-tauri/external-binaries/ollama/';
const HANZO_NODE_RESOURCES_PATH =
'./apps/hanzo-desktop/src-tauri/external-binaries/hanzo-node/';
const LLM_MODELS_PATH = './apps/hanzo-desktop/src-tauri/llm-models/';
const asBinaryName = (arch: Arch, path: string) => {
return `${path}${arch === Arch.x86_64_pc_windows_msvc ? '.exe' : ''}`;
};
const asSidecarName = (arch: Arch, path: string) => {
return path.replace(
/(.exe){0,1}$/,
`-${arch}${arch === Arch.x86_64_pc_windows_msvc ? '.exe' : ''}`,
);
};
const addExecPermissions = (path: string) => {
console.log(`Adding exec permissions (+x) to ${path}`);
return exec(`chmod +x ${path}`);
};
const downloadFile = async (url: string, path: string): Promise<void> => {
console.log(`Downloading ${url}`);
const response = await axios({
method: 'GET',
url: url,
responseType: 'stream',
onDownloadProgress: function (progressEvent) {
if (progressEvent.progress) {
console.log(`${url} progress: ${progressEvent.progress * 100}%`);
}
},
});
console.log(`ensuring file ${path}`);
await ensureFile(path);
console.log(`creating write stream ${path}`);
response.data.pipe(
createWriteStream(path, { encoding: 'binary', flags: 'w' }),
);
await new Promise<void>((resolve, reject) => {
response.data.on('end', () => {
resolve();
});
response.data.on('error', (err: any) => {
console.error(`Error downloading file: ${url}`, err);
reject(err);
});
});
console.log(`Download complete: ${url}`);
};
const downloadHanzoNodeBinary = async (arch: Arch, version: string) => {
console.log(`Downloading hanzo-node arch:${arch} version:${version}`);
const downloadUrl = `https://download.hanzo.ai/hanzo-node/binaries/production/${arch}/${version}.zip`;
const zippedPath = path.join(TEMP_PATH, `hanzo-node-${version}.zip`);
await downloadFile(downloadUrl, zippedPath);
let unzippedPath = path.join(TEMP_PATH, `hanzo-node-${version}`);
await zl.extract(zippedPath, unzippedPath, {
overwrite: true,
});
const files = await readdir(unzippedPath);
for (const file of files) {
await cp(
path.join(unzippedPath, file),
path.join(HANZO_NODE_RESOURCES_PATH, file),
{
recursive: true,
},
);
}
// They are used as sidecars in Tauri
const hanzoNodeBinaryPath = asBinaryName(
arch,
`./apps/hanzo-desktop/src-tauri/external-binaries/hanzo-node/hanzo-node`,
);
const hanzoToolsRunnerDenoBinaryPath = asBinaryName(
arch,
`./apps/hanzo-desktop/src-tauri/external-binaries/hanzo-node/hanzo-tools-runner-resources/deno`,
);
const hanzoToolsRunnerUvBinaryPath = asBinaryName(
arch,
`./apps/hanzo-desktop/src-tauri/external-binaries/hanzo-node/hanzo-tools-runner-resources/uv`,
);
await rename(
hanzoNodeBinaryPath,
asSidecarName(arch, hanzoNodeBinaryPath),
);
await rename(
hanzoToolsRunnerDenoBinaryPath,
asSidecarName(arch, hanzoToolsRunnerDenoBinaryPath),
);
await rename(
hanzoToolsRunnerUvBinaryPath,
asSidecarName(arch, hanzoToolsRunnerUvBinaryPath),
);
await addExecPermissions(asSidecarName(arch, hanzoNodeBinaryPath));
await addExecPermissions(
asSidecarName(arch, hanzoToolsRunnerDenoBinaryPath),
);
await addExecPermissions(asSidecarName(arch, hanzoToolsRunnerUvBinaryPath));
};
const downloadOllamaAarch64AppleDarwin = async (version: string) => {
const downloadUrl = `https://github.com/ollama/ollama/releases/download/${version}/Ollama-darwin.zip`;
const zippedPath = path.join(
TEMP_PATH,
`ollama-${Arch.aarch64_apple_darwin}.zip`,
);
await downloadFile(downloadUrl, zippedPath);
const unzippedPath = path.join(
TEMP_PATH,
`ollama-${Arch.aarch64_apple_darwin}-${version}`,
);
await zl.extract(zippedPath, unzippedPath, {
overwrite: true,
});
const ollamaBinaryPath = asSidecarName(
Arch.aarch64_apple_darwin,
`./apps/hanzo-desktop/src-tauri/external-binaries/ollama/ollama`,
);
await ensureFile(ollamaBinaryPath);
await copyFile(
path.join(unzippedPath, 'Ollama.app/Contents/Resources/ollama'),
ollamaBinaryPath,
);
await addExecPermissions(ollamaBinaryPath);
};
const downloadOllamax8664UnknownLinuxGnu = async (version: string) => {
const downloadUrl = `https://github.com/ollama/ollama/releases/download/${version}/ollama-linux-amd64.tgz`;
const zippedPath = path.join(TEMP_PATH, `ollama-linux-amd64-${version}.tgz`);
await downloadFile(downloadUrl, zippedPath);
const unzippedPath = path.join(TEMP_PATH, `ollama-linux-amd64-${version}`);
await mkdir(unzippedPath, { recursive: true });
await extract({
f: zippedPath,
C: unzippedPath,
strip: 0,
});
const ollamaBinaryPath = asSidecarName(
Arch.x86_64_unknown_linux_gnu,
`./apps/hanzo-desktop/src-tauri/external-binaries/ollama/ollama`,
);
await ensureFile(ollamaBinaryPath);
await copyFile(path.join(unzippedPath, 'bin/ollama'), ollamaBinaryPath);
await addExecPermissions(ollamaBinaryPath);
};
const downloadOllamaAarch64UnknownLinuxGnu = async (version: string) => {
const downloadUrl = `https://github.com/ollama/ollama/releases/download/${version}/ollama-linux-arm64.tgz`;
const zippedPath = path.join(TEMP_PATH, `ollama-linux-arm64-${version}.tgz`);
await downloadFile(downloadUrl, zippedPath);
const unzippedPath = path.join(TEMP_PATH, `ollama-linux-arm64-${version}`);
await mkdir(unzippedPath, { recursive: true });
await extract({
f: zippedPath,
C: unzippedPath,
strip: 0,
});
const ollamaBinaryPath = asSidecarName(
Arch.aarch64_unknown_linux_gnu,
`./apps/hanzo-desktop/src-tauri/external-binaries/ollama/ollama`,
);
await ensureFile(ollamaBinaryPath);
await copyFile(path.join(unzippedPath, 'bin/ollama'), ollamaBinaryPath);
await addExecPermissions(ollamaBinaryPath);
};
const downloadOllamax8664PcWindowsMsvc = async (version: string) => {
await downloadOllamax8664PcWindowsMsvcNvidia(version);
await downloadOllamax8664PcWindowsMsvcRocm(version);
};
const downloadOllamax8664PcWindowsMsvcRocm = async (version: string) => {
const downloadUrl = `https://github.com/ollama/ollama/releases/download/${version}/ollama-windows-amd64-rocm.zip`;
const zippedPath = path.join(
TEMP_PATH,
`ollama-windows-amd64-rocm-${version}.zip`,
);
await downloadFile(downloadUrl, zippedPath);
const unzippedPath = path.join(
TEMP_PATH,
`ollama-windows-amd64-rocm-${version}`,
);
await zl.extract(zippedPath, unzippedPath, {
overwrite: true,
});
const files = await readdir(unzippedPath);
await cp(
path.join(unzippedPath, 'lib', 'ollama', 'rocm'),
path.join(OLLAMA_RESOURCES_PATH, 'lib', 'rocm'),
{
recursive: true,
},
);
};
const downloadOllamax8664PcWindowsMsvcNvidia = async (version: string) => {
const downloadUrl = `https://github.com/ollama/ollama/releases/download/${version}/ollama-windows-amd64.zip`;
const zippedPath = path.join(
TEMP_PATH,
`ollama-windows-amd64-${version}.zip`,
);
await downloadFile(downloadUrl, zippedPath);
const unzippedPath = path.join(TEMP_PATH, `ollama-windows-amd64-${version}`);
await zl.extract(zippedPath, unzippedPath, {
overwrite: true,
});
const files = await readdir(unzippedPath);
for (const file of files) {
await cp(
path.join(unzippedPath, file),
path.join(OLLAMA_RESOURCES_PATH, file),
{
recursive: true,
},
);
}
const ollamaBinaryPath = asBinaryName(
Arch.x86_64_pc_windows_msvc,
`./apps/hanzo-desktop/src-tauri/external-binaries/ollama/ollama`,
);
await rename(
ollamaBinaryPath,
asSidecarName(Arch.x86_64_pc_windows_msvc, ollamaBinaryPath),
);
await addExecPermissions(
asSidecarName(Arch.x86_64_pc_windows_msvc, ollamaBinaryPath),
);
};
const downloadOllama = {
[Arch.aarch64_apple_darwin]: downloadOllamaAarch64AppleDarwin,
[Arch.x86_64_unknown_linux_gnu]: downloadOllamax8664UnknownLinuxGnu,
[Arch.aarch64_unknown_linux_gnu]: downloadOllamaAarch64UnknownLinuxGnu,
[Arch.x86_64_pc_windows_msvc]: downloadOllamax8664PcWindowsMsvc,
};
const downloadEmbeddingModel = async () => {
console.log(`Downloading embedding model`);
const downloadUrl = `https://download.hanzo.ai/llm-models/embeddinggemma-300M-BF16.gguf`;
await downloadFile(
downloadUrl,
path.join(LLM_MODELS_PATH, 'embeddinggemma-300M-BF16.gguf'),
);
};
export const main = async () => {
await downloadHanzoNodeBinary(env.ARCH, env.HANZO_NODE_VERSION);
await downloadOllama[env.ARCH](env.OLLAMA_VERSION);
await downloadEmbeddingModel();
};
main().catch(console.error);