@@ -222,9 +222,9 @@ function pickAssetNamePattern(platform: Platform, arch: Arch): (name: string) =>
222222 }
223223
224224 // Strict: only Ubuntu Vulkan x64 builds:
225- // llama-b*-bin-ubuntu-vulkan-x64.zip
225+ // llama-b*-bin-ubuntu-vulkan-x64.tar.gz
226226 if (
227- lower . endsWith ( '.zip ' ) &&
227+ lower . endsWith ( '.tar.gz ' ) &&
228228 lower . includes ( '-ubuntu-vulkan-x64' )
229229 ) {
230230 return true ;
@@ -411,7 +411,8 @@ async function downloadFile(
411411/**
412412* Extract a zip archive into targetDir and resolve when complete.
413413* Uses native unzip on macOS to preserve file permissions and attributes.
414- * Uses "unzipper" package on Linux and Windows.
414+ * Uses "unzipper" package on Windows.
415+ * Note: Ubuntu uses tar.gz archives, not zip (see extractTarGz function).
415416*/
416417async function extractZip ( assetPath : string , targetDir : string ) : Promise < void > {
417418 ensureDirSync ( targetDir ) ;
@@ -460,6 +461,33 @@ async function extractZip(assetPath: string, targetDir: string): Promise<void> {
460461 } ) ;
461462}
462463
464+ /**
465+ * Extract a tar.gz archive into targetDir using native tar command.
466+ * This is used for Ubuntu builds which are distributed as .tar.gz files.
467+ */
468+ async function extractTarGz ( assetPath : string , targetDir : string ) : Promise < void > {
469+ ensureDirSync ( targetDir ) ;
470+ logDebug ( 'Extracting llama.cpp tar.gz archive' , { assetPath, targetDir } ) ;
471+
472+ // eslint-disable-next-line @typescript-eslint/no-var-requires
473+ const { execFile } = require ( 'child_process' ) ;
474+ // eslint-disable-next-line @typescript-eslint/no-var-requires
475+ const { promisify } = require ( 'util' ) ;
476+ const execFileAsync = promisify ( execFile ) ;
477+
478+ try {
479+ // -x: extract
480+ // -z: decompress with gzip
481+ // -f: file
482+ // -C: change to directory
483+ await execFileAsync ( 'tar' , [ '-xzf' , assetPath , '-C' , targetDir ] ) ;
484+ logDebug ( 'Tar.gz extraction completed' , { assetPath, targetDir } ) ;
485+ } catch ( err ) {
486+ logError ( 'Tar extraction failed' , err ) ;
487+ throw new Error ( `Failed to extract tar.gz: ${ err } ` ) ;
488+ }
489+ }
490+
463491/**
464492* Given the downloaded asset and platform, locate the llama-server binary.
465493*
@@ -469,6 +497,7 @@ async function extractZip(assetPath: string, targetDir: string): Promise<void> {
469497* Behavior:
470498* - If asset is a Windows .exe, return as-is.
471499* - If asset is a .zip, extract into INSTALL_DIR and search for llama-server.
500+ * - If asset is a .tar.gz (Ubuntu), extract using tar command and search for llama-server.
472501* - Ensure the found binary is marked executable.
473502* - Otherwise, fall back to assetPath to match previous behavior.
474503*/
@@ -550,6 +579,73 @@ async function inferBinaryPathFromAsset(
550579 throw new Error ( `llama-server binary not found in extracted archive: ${ assetPath } ` ) ;
551580 }
552581
582+ // Tar.gz archives: extract then search for llama-server
583+ if ( lower . endsWith ( '.tar.gz' ) ) {
584+ const extractRoot = INSTALL_DIR ;
585+ await extractTarGz ( assetPath , extractRoot ) ;
586+
587+ // Expected: llama-{version}-bin-ubuntu-vulkan-x64/build/bin/llama-server
588+ const entries = fs . readdirSync ( extractRoot ) ;
589+ for ( const entry of entries ) {
590+ const full = path . join ( extractRoot , entry ) ;
591+ if ( ! fs . statSync ( full ) . isDirectory ( ) ) continue ;
592+
593+ const buildBin = path . join ( full , 'build' , 'bin' ) ;
594+ if ( fs . existsSync ( buildBin ) && fs . statSync ( buildBin ) . isDirectory ( ) ) {
595+ const binEntries = fs . readdirSync ( buildBin ) ;
596+ for ( const be of binEntries ) {
597+ const candidate = path . join ( buildBin , be ) ;
598+ const name = be . toLowerCase ( ) ;
599+ if (
600+ fs . statSync ( candidate ) . isFile ( ) &&
601+ ( name === 'llama-server' || name === 'llama-server.exe' )
602+ ) {
603+ try {
604+ fs . chmodSync ( candidate , 0o755 ) ;
605+ } catch {
606+ // best-effort; ignore chmod failures on non-POSIX
607+ }
608+ logDebug ( 'Resolved llama-server binary inside archive' , {
609+ assetPath,
610+ candidate,
611+ } ) ;
612+ return candidate ;
613+ }
614+ }
615+ }
616+ }
617+
618+ // Fallback: recursive search under extractRoot
619+ const stack : string [ ] = [ extractRoot ] ;
620+ while ( stack . length ) {
621+ const dir = stack . pop ( ) as string ;
622+ const children = fs . readdirSync ( dir ) ;
623+ for ( const child of children ) {
624+ const full = path . join ( dir , child ) ;
625+ const stat = fs . statSync ( full ) ;
626+ if ( stat . isDirectory ( ) ) {
627+ stack . push ( full ) ;
628+ } else if ( stat . isFile ( ) ) {
629+ const name = child . toLowerCase ( ) ;
630+ if ( name === 'llama-server' || name === 'llama-server.exe' ) {
631+ try {
632+ fs . chmodSync ( full , 0o755 ) ;
633+ } catch {
634+ // ignore chmod errors
635+ }
636+ logDebug ( 'Resolved llama-server binary via recursive search' , {
637+ assetPath,
638+ candidate : full ,
639+ } ) ;
640+ return full ;
641+ }
642+ }
643+ }
644+ }
645+
646+ throw new Error ( `llama-server binary not found in extracted archive: ${ assetPath } ` ) ;
647+ }
648+
553649 // Non-zip assets: preserve previous behavior.
554650 return assetPath ;
555651}
0 commit comments