mirror of
https://github.com/lone-cloud/gerbil
synced 2026-06-03 09:33:10 -07:00
add windows rocm build, better support for binaries that unpack without a launcher
This commit is contained in:
parent
c8098a3e68
commit
e2ff70a2dc
6 changed files with 321 additions and 194 deletions
|
|
@ -74,12 +74,6 @@ The AUR package automatically handles installation, desktop integration, and sys
|
|||
</div>
|
||||
<!-- markdownlint-enable MD033 -->
|
||||
|
||||
### Windows ROCm Support
|
||||
|
||||
There is ROCm Windows support maintained by YellowRoseCx in a separate fork.
|
||||
Unfortunately it does not properly support unpacking, which would greatly diminish its performance and provide a poor UX when used alongside this app.
|
||||
For Friendly Kobold to work with this fork, [this issue must be fixed first](https://github.com/YellowRoseCx/koboldcpp-rocm/issues/129).
|
||||
|
||||
### Future features
|
||||
|
||||
Not all koboldcpp features have currently been ported over to the UI. As a workaround one may use the "Additional arguments" on the "Advanced" tab of the launcher to provide additional command line arguments if you know them.
|
||||
|
|
|
|||
|
|
@ -39,9 +39,11 @@ export const ASSET_SUFFIXES = {
|
|||
} as const;
|
||||
|
||||
export const ROCM = {
|
||||
BINARY_NAME: 'koboldcpp-linux-x64-rocm',
|
||||
DOWNLOAD_URL: 'https://koboldai.org/cpplinuxrocm',
|
||||
SIZE_BYTES_APPROX: 1024 * 1024 * 1024,
|
||||
LINUX: {
|
||||
BINARY_NAME: 'koboldcpp-linux-x64-rocm',
|
||||
DOWNLOAD_URL: 'https://koboldai.org/cpplinuxrocm',
|
||||
SIZE_BYTES_APPROX: 1024 * 1024 * 1024,
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const FRONTENDS = {
|
||||
|
|
|
|||
|
|
@ -4,11 +4,8 @@ import {
|
|||
existsSync,
|
||||
readdirSync,
|
||||
statSync,
|
||||
createWriteStream,
|
||||
chmodSync,
|
||||
readFileSync,
|
||||
writeFileSync,
|
||||
unlinkSync,
|
||||
} from 'fs';
|
||||
import { rm } from 'fs/promises';
|
||||
import { dialog } from 'electron';
|
||||
|
|
@ -16,13 +13,11 @@ import { dialog } from 'electron';
|
|||
import { execa } from 'execa';
|
||||
import { terminateProcess } from '@/utils/process';
|
||||
import { getROCmDownload } from '@/utils/rocm';
|
||||
import { got } from 'got';
|
||||
import { pipeline } from 'stream/promises';
|
||||
import { ConfigManager } from '@/main/managers/ConfigManager';
|
||||
import { LogManager } from '@/main/managers/LogManager';
|
||||
import { WindowManager } from '@/main/managers/WindowManager';
|
||||
import { PRODUCT_NAME, SERVER_READY_SIGNALS } from '@/constants';
|
||||
import { stripAssetExtensions } from '@/utils/version';
|
||||
import { downloadAndUnpackBinary } from '@/utils/server/download';
|
||||
import type {
|
||||
GitHubAsset,
|
||||
InstalledVersion,
|
||||
|
|
@ -82,95 +77,34 @@ export class KoboldCppManager {
|
|||
asset: GitHubAsset,
|
||||
onProgress?: (progress: number) => void
|
||||
): Promise<string> {
|
||||
const tempPackedFilePath = join(this.installDir, `${asset.name}.packed`);
|
||||
const baseFilename = stripAssetExtensions(asset.name);
|
||||
const unpackedDirPath = join(this.installDir, baseFilename);
|
||||
|
||||
if (asset.isUpdate && existsSync(unpackedDirPath)) {
|
||||
try {
|
||||
if (this.koboldProcess && !this.koboldProcess.killed) {
|
||||
this.windowManager.sendKoboldOutput(
|
||||
'Stopping KoboldCpp process before update...'
|
||||
);
|
||||
|
||||
await this.cleanup();
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
}
|
||||
|
||||
await this.removeDirectoryWithRetry(unpackedDirPath);
|
||||
} catch (error) {
|
||||
this.logManager.logError(
|
||||
'Failed to remove existing directory for update:',
|
||||
error as Error
|
||||
);
|
||||
throw new Error(
|
||||
`Cannot update: Failed to remove existing installation. ` +
|
||||
`Please ensure KoboldCpp is stopped and try again. ` +
|
||||
`Error: ${(error as Error).message}`
|
||||
);
|
||||
const result = await downloadAndUnpackBinary(
|
||||
{
|
||||
name: asset.name,
|
||||
url: asset.browser_download_url,
|
||||
size: asset.size,
|
||||
type: 'asset' as const,
|
||||
},
|
||||
{
|
||||
installDir: this.installDir,
|
||||
onProgress,
|
||||
isUpdate: asset.isUpdate,
|
||||
wasCurrentBinary: asset.wasCurrentBinary,
|
||||
logManager: this.logManager,
|
||||
configManager: this.configManager,
|
||||
windowManager: this.windowManager,
|
||||
unpackFunction: this.unpackKoboldCpp.bind(this),
|
||||
getLauncherPath: this.getLauncherPath.bind(this),
|
||||
removeDirectoryWithRetry: this.removeDirectoryWithRetry.bind(this),
|
||||
cleanup: this.cleanup.bind(this),
|
||||
koboldProcess: this.koboldProcess || undefined,
|
||||
}
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Download failed');
|
||||
}
|
||||
|
||||
const writer = createWriteStream(tempPackedFilePath);
|
||||
let downloadedBytes = 0;
|
||||
const totalBytes = asset.size;
|
||||
|
||||
try {
|
||||
await pipeline(
|
||||
got
|
||||
.stream(asset.browser_download_url)
|
||||
.on('downloadProgress', (progress) => {
|
||||
downloadedBytes = progress.transferred;
|
||||
if (onProgress && totalBytes > 0) {
|
||||
onProgress((downloadedBytes / totalBytes) * 100);
|
||||
}
|
||||
}),
|
||||
writer
|
||||
);
|
||||
} catch (error) {
|
||||
throw new Error(`Download failed: ${(error as Error).message}`);
|
||||
}
|
||||
|
||||
if (process.platform !== 'win32') {
|
||||
try {
|
||||
chmodSync(tempPackedFilePath, 0o755);
|
||||
} catch (error) {
|
||||
this.logManager.logError(
|
||||
'Failed to make binary executable:',
|
||||
error as Error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await this.unpackKoboldCpp(tempPackedFilePath, unpackedDirPath);
|
||||
|
||||
try {
|
||||
unlinkSync(tempPackedFilePath);
|
||||
} catch (error) {
|
||||
this.logManager.logError(
|
||||
'Failed to cleanup packed file:',
|
||||
error as Error
|
||||
);
|
||||
}
|
||||
|
||||
const launcherPath = this.getLauncherPath(unpackedDirPath);
|
||||
if (launcherPath && existsSync(launcherPath)) {
|
||||
const currentBinary = this.configManager.getCurrentKoboldBinary();
|
||||
if (!currentBinary || (asset.isUpdate && asset.wasCurrentBinary)) {
|
||||
this.configManager.setCurrentKoboldBinary(launcherPath);
|
||||
}
|
||||
|
||||
this.windowManager.sendToRenderer('versions-updated');
|
||||
|
||||
return launcherPath;
|
||||
} else {
|
||||
throw new Error('Failed to find koboldcpp-launcher after unpacking');
|
||||
}
|
||||
} catch (error) {
|
||||
this.logManager.logError('Failed to unpack KoboldCpp:', error as Error);
|
||||
throw error;
|
||||
}
|
||||
return result.path!;
|
||||
}
|
||||
|
||||
private async unpackKoboldCpp(
|
||||
|
|
@ -562,91 +496,23 @@ export class KoboldCppManager {
|
|||
};
|
||||
}
|
||||
|
||||
const tempPackedFilePath = join(
|
||||
this.installDir,
|
||||
`${rocmInfo.name}.packed`
|
||||
return await downloadAndUnpackBinary(
|
||||
{
|
||||
name: rocmInfo.name,
|
||||
url: rocmInfo.url,
|
||||
size: rocmInfo.size,
|
||||
type: 'rocm' as const,
|
||||
},
|
||||
{
|
||||
installDir: this.installDir,
|
||||
onProgress,
|
||||
logManager: this.logManager,
|
||||
configManager: this.configManager,
|
||||
windowManager: this.windowManager,
|
||||
unpackFunction: this.unpackKoboldCpp.bind(this),
|
||||
getLauncherPath: this.getLauncherPath.bind(this),
|
||||
}
|
||||
);
|
||||
const baseFilename = stripAssetExtensions(rocmInfo.name);
|
||||
const unpackedDirPath = join(this.installDir, baseFilename);
|
||||
|
||||
const response = await fetch(rocmInfo.url);
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Failed to download: ${response.statusText}`,
|
||||
};
|
||||
}
|
||||
|
||||
const writer = createWriteStream(tempPackedFilePath);
|
||||
let downloadedBytes = 0;
|
||||
const totalBytes = rocmInfo.size;
|
||||
|
||||
try {
|
||||
await pipeline(
|
||||
got.stream(rocmInfo.url).on('downloadProgress', (progress) => {
|
||||
downloadedBytes = progress.transferred;
|
||||
if (onProgress && totalBytes > 0) {
|
||||
onProgress((downloadedBytes / totalBytes) * 100);
|
||||
}
|
||||
}),
|
||||
writer
|
||||
);
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Download failed: ${(error as Error).message}`,
|
||||
};
|
||||
}
|
||||
|
||||
if (process.platform !== 'win32') {
|
||||
try {
|
||||
chmodSync(tempPackedFilePath, 0o755);
|
||||
} catch (error) {
|
||||
this.logManager.logError(
|
||||
'Failed to make ROCm binary executable:',
|
||||
error as Error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await this.unpackKoboldCpp(tempPackedFilePath, unpackedDirPath);
|
||||
|
||||
try {
|
||||
unlinkSync(tempPackedFilePath);
|
||||
} catch (error) {
|
||||
this.logManager.logError(
|
||||
'Failed to cleanup packed ROCm file:',
|
||||
error as Error
|
||||
);
|
||||
}
|
||||
|
||||
const launcherPath = this.getLauncherPath(unpackedDirPath);
|
||||
if (launcherPath && existsSync(launcherPath)) {
|
||||
const currentBinary = this.configManager.getCurrentKoboldBinary();
|
||||
if (!currentBinary) {
|
||||
this.configManager.setCurrentKoboldBinary(launcherPath);
|
||||
}
|
||||
|
||||
this.windowManager.sendToRenderer('versions-updated');
|
||||
|
||||
return {
|
||||
success: true,
|
||||
path: launcherPath,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Failed to find koboldcpp-launcher after unpacking ROCm',
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
this.logManager.logError('Failed to unpack ROCm:', error as Error);
|
||||
return {
|
||||
success: false,
|
||||
error: `Failed to unpack ROCm: ${(error as Error).message}`,
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { ROCM } from '@/constants';
|
|||
export const formatDownloadSize = (size: number, url?: string): string => {
|
||||
if (!size) return '';
|
||||
|
||||
const isApproximateSize = url?.includes(ROCM.DOWNLOAD_URL);
|
||||
const isApproximateSize = url?.includes(ROCM.LINUX.DOWNLOAD_URL);
|
||||
|
||||
return isApproximateSize
|
||||
? `~${formatFileSizeInMB(size)}`
|
||||
|
|
|
|||
|
|
@ -1,13 +1,20 @@
|
|||
import { GITHUB_API, ROCM } from '@/constants';
|
||||
import type { DownloadItem } from '@/types/electron';
|
||||
import type { GitHubAsset } from '@/types';
|
||||
|
||||
export async function getROCmDownload(
|
||||
platform: string
|
||||
): Promise<DownloadItem | null> {
|
||||
if (platform !== 'linux') {
|
||||
return null;
|
||||
if (platform === 'linux') {
|
||||
return getLinuxROCmDownload();
|
||||
} else if (platform === 'win32') {
|
||||
return getWindowsROCmDownload();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function getLinuxROCmDownload(): Promise<DownloadItem | null> {
|
||||
try {
|
||||
const response = await fetch(GITHUB_API.LATEST_RELEASE_URL);
|
||||
if (!response.ok) {
|
||||
|
|
@ -18,19 +25,54 @@ export async function getROCmDownload(
|
|||
const version = latestRelease?.tag_name?.replace(/^v/, '') || 'unknown';
|
||||
|
||||
return {
|
||||
name: ROCM.BINARY_NAME,
|
||||
url: ROCM.DOWNLOAD_URL,
|
||||
size: ROCM.SIZE_BYTES_APPROX,
|
||||
name: ROCM.LINUX.BINARY_NAME,
|
||||
url: ROCM.LINUX.DOWNLOAD_URL,
|
||||
size: ROCM.LINUX.SIZE_BYTES_APPROX,
|
||||
version,
|
||||
type: 'rocm',
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
name: ROCM.BINARY_NAME,
|
||||
url: ROCM.DOWNLOAD_URL,
|
||||
size: ROCM.SIZE_BYTES_APPROX,
|
||||
name: ROCM.LINUX.BINARY_NAME,
|
||||
url: ROCM.LINUX.DOWNLOAD_URL,
|
||||
size: ROCM.LINUX.SIZE_BYTES_APPROX,
|
||||
version: 'unknown',
|
||||
type: 'rocm',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function getWindowsROCmDownload(): Promise<DownloadItem | null> {
|
||||
try {
|
||||
const response = await fetch(GITHUB_API.ROCM_LATEST_RELEASE_URL);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const latestRelease = await response.json();
|
||||
const version = latestRelease?.tag_name?.replace(/^v/, '') || 'unknown';
|
||||
|
||||
const windowsAsset =
|
||||
latestRelease.assets?.find(
|
||||
(asset: GitHubAsset) => asset.name === 'koboldcpp_rocm.exe'
|
||||
) ||
|
||||
latestRelease.assets?.find(
|
||||
(asset: GitHubAsset) =>
|
||||
asset.name.includes('koboldcpp_rocm') && asset.name.endsWith('.exe')
|
||||
);
|
||||
|
||||
if (!windowsAsset) {
|
||||
throw new Error('Windows ROCm executable not found in release');
|
||||
}
|
||||
|
||||
return {
|
||||
name: windowsAsset.name.replace('.exe', ''),
|
||||
url: windowsAsset.browser_download_url,
|
||||
size: windowsAsset.size,
|
||||
version,
|
||||
type: 'rocm',
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
223
src/utils/server/download.ts
Normal file
223
src/utils/server/download.ts
Normal file
|
|
@ -0,0 +1,223 @@
|
|||
import { createWriteStream, chmodSync, existsSync } from 'fs';
|
||||
import { unlink, rename, mkdir } from 'fs/promises';
|
||||
import { join } from 'path';
|
||||
import { pipeline } from 'stream/promises';
|
||||
import got from 'got';
|
||||
import type { LogManager } from '@/main/managers/LogManager';
|
||||
import type { ConfigManager } from '@/main/managers/ConfigManager';
|
||||
import type { WindowManager } from '@/main/managers/WindowManager';
|
||||
import type { DownloadItem } from '@/types/electron';
|
||||
import { stripAssetExtensions } from '@/utils/version';
|
||||
import type { ChildProcess } from 'child_process';
|
||||
|
||||
export interface DownloadOptions {
|
||||
installDir: string;
|
||||
onProgress?: (progress: number) => void;
|
||||
isUpdate?: boolean;
|
||||
wasCurrentBinary?: boolean;
|
||||
logManager: LogManager;
|
||||
configManager: ConfigManager;
|
||||
windowManager: WindowManager;
|
||||
unpackFunction: (packedPath: string, unpackDir: string) => Promise<void>;
|
||||
getLauncherPath: (unpackedDir: string) => string | null;
|
||||
removeDirectoryWithRetry?: (dirPath: string) => Promise<void>;
|
||||
cleanup?: () => Promise<void>;
|
||||
koboldProcess?: ChildProcess;
|
||||
}
|
||||
|
||||
export interface DownloadResult {
|
||||
success: boolean;
|
||||
path?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
async function handleExistingDirectory(
|
||||
unpackedDirPath: string,
|
||||
isUpdate: boolean,
|
||||
koboldProcess: ChildProcess | undefined,
|
||||
windowManager: WindowManager,
|
||||
cleanup: (() => Promise<void>) | undefined,
|
||||
removeDirectoryWithRetry: ((dirPath: string) => Promise<void>) | undefined,
|
||||
logManager: LogManager
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
if (!isUpdate || !existsSync(unpackedDirPath)) {
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
try {
|
||||
if (koboldProcess && !koboldProcess.killed) {
|
||||
windowManager.sendKoboldOutput(
|
||||
'Stopping KoboldCpp process before update...'
|
||||
);
|
||||
|
||||
if (cleanup) {
|
||||
await cleanup();
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
}
|
||||
|
||||
if (removeDirectoryWithRetry) {
|
||||
await removeDirectoryWithRetry(unpackedDirPath);
|
||||
}
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
logManager.logError(
|
||||
'Failed to remove existing directory for update:',
|
||||
error as Error
|
||||
);
|
||||
return {
|
||||
success: false,
|
||||
error:
|
||||
`Cannot update: Failed to remove existing installation. ` +
|
||||
`Please ensure KoboldCpp is stopped and try again. ` +
|
||||
`Error: ${(error as Error).message}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadFile(
|
||||
item: DownloadItem,
|
||||
tempPackedFilePath: string,
|
||||
onProgress: ((progress: number) => void) | undefined,
|
||||
logManager: LogManager
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
const writer = createWriteStream(tempPackedFilePath);
|
||||
let downloadedBytes = 0;
|
||||
const totalBytes = item.size;
|
||||
|
||||
try {
|
||||
await pipeline(
|
||||
got.stream(item.url).on('downloadProgress', (progress) => {
|
||||
downloadedBytes = progress.transferred;
|
||||
if (onProgress && totalBytes > 0) {
|
||||
onProgress((downloadedBytes / totalBytes) * 100);
|
||||
}
|
||||
}),
|
||||
writer
|
||||
);
|
||||
|
||||
if (process.platform !== 'win32') {
|
||||
try {
|
||||
chmodSync(tempPackedFilePath, 0o755);
|
||||
} catch (error) {
|
||||
logManager.logError(
|
||||
'Failed to make binary executable:',
|
||||
error as Error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Download failed: ${(error as Error).message}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function downloadAndUnpackBinary(
|
||||
item: DownloadItem,
|
||||
options: DownloadOptions
|
||||
): Promise<DownloadResult> {
|
||||
const {
|
||||
installDir,
|
||||
onProgress,
|
||||
isUpdate,
|
||||
wasCurrentBinary,
|
||||
logManager,
|
||||
configManager,
|
||||
windowManager,
|
||||
unpackFunction,
|
||||
getLauncherPath,
|
||||
removeDirectoryWithRetry,
|
||||
cleanup,
|
||||
koboldProcess,
|
||||
} = options;
|
||||
|
||||
const tempPackedFilePath = join(installDir, `${item.name}.packed`);
|
||||
const baseFilename = stripAssetExtensions(item.name);
|
||||
const unpackedDirPath = join(installDir, baseFilename);
|
||||
|
||||
const dirResult = await handleExistingDirectory(
|
||||
unpackedDirPath,
|
||||
Boolean(isUpdate),
|
||||
koboldProcess,
|
||||
windowManager,
|
||||
cleanup,
|
||||
removeDirectoryWithRetry,
|
||||
logManager
|
||||
);
|
||||
|
||||
if (!dirResult.success) {
|
||||
return { success: false, error: dirResult.error };
|
||||
}
|
||||
|
||||
const downloadResult = await downloadFile(
|
||||
item,
|
||||
tempPackedFilePath,
|
||||
onProgress,
|
||||
logManager
|
||||
);
|
||||
if (!downloadResult.success) {
|
||||
return { success: false, error: downloadResult.error };
|
||||
}
|
||||
|
||||
try {
|
||||
await mkdir(unpackedDirPath, { recursive: true });
|
||||
await unpackFunction(tempPackedFilePath, unpackedDirPath);
|
||||
|
||||
let launcherPath = getLauncherPath(unpackedDirPath);
|
||||
|
||||
if (!launcherPath || !existsSync(launcherPath)) {
|
||||
const expectedLauncherName =
|
||||
process.platform === 'win32'
|
||||
? 'koboldcpp-launcher.exe'
|
||||
: 'koboldcpp-launcher';
|
||||
const newLauncherPath = join(unpackedDirPath, expectedLauncherName);
|
||||
|
||||
if (existsSync(tempPackedFilePath)) {
|
||||
try {
|
||||
await rename(tempPackedFilePath, newLauncherPath);
|
||||
launcherPath = newLauncherPath;
|
||||
} catch (error) {
|
||||
logManager.logError(
|
||||
'Failed to rename binary as launcher:',
|
||||
error as Error
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
await unlink(tempPackedFilePath);
|
||||
} catch (error) {
|
||||
logManager.logError('Failed to cleanup packed file:', error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
if (launcherPath && existsSync(launcherPath)) {
|
||||
const currentBinary = configManager.getCurrentKoboldBinary();
|
||||
if (!currentBinary || (isUpdate && wasCurrentBinary)) {
|
||||
configManager.setCurrentKoboldBinary(launcherPath);
|
||||
}
|
||||
|
||||
windowManager.sendToRenderer('versions-updated');
|
||||
|
||||
return {
|
||||
success: true,
|
||||
path: launcherPath,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Failed to find or create koboldcpp launcher',
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
logManager.logError('Failed to unpack binary:', error as Error);
|
||||
return {
|
||||
success: false,
|
||||
error: `Failed to unpack binary: ${(error as Error).message}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue