2025-01-22 16:18:30 +01:00
import errno
import glob
import json
import os
from queue import Queue
import re
import shlex
import shutil
import stat
from string import Template
import subprocess
2026-04-27 19:21:50 -07:00
import tempfile
2025-01-22 16:18:30 +01:00
import threading
import time
import traceback
from typing import Callable
import webbrowser
import bpy
from lnx import assets
from lnx . exporter import LeenkxExporter
import lnx . lib . make_datas
import lnx . lib . server
import lnx . live_patch as live_patch
import lnx . log as log
import lnx . make_logic as make_logic
import lnx . make_renderpath as make_renderpath
import lnx . make_state as state
import lnx . make_world as make_world
import lnx . utils
import lnx . utils_vs
import lnx . write_data as write_data
if lnx . is_reload ( __name__ ) :
assets = lnx . reload_module ( assets )
lnx . exporter = lnx . reload_module ( lnx . exporter )
from lnx . exporter import LeenkxExporter
lnx . lib . make_datas = lnx . reload_module ( lnx . lib . make_datas )
lnx . lib . server = lnx . reload_module ( lnx . lib . server )
live_patch = lnx . reload_module ( live_patch )
log = lnx . reload_module ( log )
make_logic = lnx . reload_module ( make_logic )
make_renderpath = lnx . reload_module ( make_renderpath )
state = lnx . reload_module ( state )
make_world = lnx . reload_module ( make_world )
lnx . utils = lnx . reload_module ( lnx . utils )
lnx . utils_vs = lnx . reload_module ( lnx . utils_vs )
write_data = lnx . reload_module ( write_data )
else :
lnx . enable_reload ( __name__ )
scripts_mtime = 0 # Monitor source changes
profile_time = 0
# Queue of threads and their done callbacks. Item format: [thread, done]
thread_callback_queue = Queue ( maxsize = 0 )
def run_proc ( cmd , done : Callable ) - > subprocess . Popen :
""" Creates a subprocess with the given command and returns it.
If Blender is not running in background mode , a thread is spawned
that waits until the subprocess has finished executing to not freeze
the UI , otherwise ( in background mode ) execution is blocked until
the subprocess has finished .
If ` done ` is not ` None ` , it is called afterwards in the main thread .
"""
use_thread = not bpy . app . background
def wait_for_proc ( proc : subprocess . Popen ) :
proc . wait ( )
if use_thread :
# Put the done callback into the callback queue so that it
# can be received by a polling function in the main thread
thread_callback_queue . put ( [ threading . current_thread ( ) , done ] , block = True )
2025-03-23 13:17:06 +00:00
while not thread_callback_queue . empty ( ) :
thread , callback = thread_callback_queue . get ( )
if callback is not None :
callback ( )
2025-01-22 16:18:30 +01:00
else :
done ( )
print ( * cmd )
p = subprocess . Popen ( cmd )
if use_thread :
threading . Thread ( target = wait_for_proc , args = ( p , ) ) . start ( )
else :
wait_for_proc ( p )
return p
def compile_shader_pass ( res , raw_shaders_path , shader_name , defs , make_variants ) :
os . chdir ( raw_shaders_path + ' / ' + shader_name )
# Open json file
json_name = shader_name + ' .json '
with open ( json_name , encoding = ' utf-8 ' ) as f :
json_file = f . read ( )
json_data = json . loads ( json_file )
fp = lnx . utils . get_fp_build ( )
lnx . lib . make_datas . make ( res , shader_name , json_data , fp , defs , make_variants )
path = fp + ' /compiled/Shaders '
contexts = json_data [ ' contexts ' ]
for ctx in contexts :
for s in [ ' vertex_shader ' , ' fragment_shader ' , ' geometry_shader ' , ' tesscontrol_shader ' , ' tesseval_shader ' ] :
if s in ctx :
shutil . copy ( ctx [ s ] , path + ' / ' + ctx [ s ] . split ( ' / ' ) [ - 1 ] )
def remove_readonly ( func , path , excinfo ) :
os . chmod ( path , stat . S_IWRITE )
func ( path )
2025-09-19 19:00:50 +00:00
2026-05-12 23:54:06 -07:00
linked_blend_paths = [ ]
linked_scenes = [ ]
2025-09-19 19:00:50 +00:00
def load_external_blends ( ) :
2026-05-12 23:54:06 -07:00
global linked_scenes
global linked_blend_paths
2025-09-19 19:00:50 +00:00
wrd = bpy . data . worlds [ ' Lnx ' ]
if not hasattr ( wrd , ' lnx_external_blends_path ' ) :
return
external_path = getattr ( wrd , ' lnx_external_blends_path ' , ' ' )
if not external_path or not external_path . strip ( ) :
return
abs_path = bpy . path . abspath ( external_path . strip ( ) )
if not os . path . exists ( abs_path ) :
return
# Walk recursively through all subdirs
for root , dirs , files in os . walk ( abs_path ) :
for filename in files :
if not filename . endswith ( " .blend " ) :
continue
blend_path = os . path . join ( root , filename )
try :
with bpy . data . libraries . load ( blend_path , link = True ) as ( data_from , data_to ) :
data_to . scenes = list ( data_from . scenes )
2026-05-12 23:54:06 -07:00
linked_blend_paths . append ( blend_path )
2025-09-19 19:00:50 +00:00
for scn in data_to . scenes :
2026-05-12 23:54:06 -07:00
if scn is not None and scn not in linked_scenes :
linked_scenes . append ( scn )
2025-09-19 19:00:50 +00:00
log . info ( f " Loaded external blend: { blend_path } " )
except Exception as e :
log . error ( f " Failed to load external blend { blend_path } : { e } " )
def clear_external_scenes ( ) :
2026-05-12 23:54:06 -07:00
global linked_blend_paths
global linked_scenes
if not linked_scenes and not linked_blend_paths :
2025-09-19 19:00:50 +00:00
return
2026-05-12 23:54:06 -07:00
for scn in linked_scenes :
2025-09-19 19:00:50 +00:00
try :
bpy . data . scenes . remove ( scn , do_unlink = True )
except Exception as e :
log . error ( f " Failed to remove scene { scn . name } : { e } " )
for lib in list ( bpy . data . libraries ) :
try :
2026-05-12 23:54:06 -07:00
if lib . users == 0 or lib . filepath in linked_blend_paths :
2025-09-19 19:00:50 +00:00
bpy . data . libraries . remove ( lib )
except Exception as e :
log . error ( f " Failed to remove library { lib . name } : { e } " )
try :
bpy . ops . outliner . orphans_purge ( do_local_ids = True , do_linked_ids = True , do_recursive = True )
except Exception as e :
log . error ( f " Failed to purge orphan data: { e } " )
2026-05-12 23:54:06 -07:00
linked_scenes = [ ]
linked_blend_paths = [ ]
2025-09-19 19:00:50 +00:00
2025-01-22 16:18:30 +01:00
def export_data ( fp , sdk_path ) :
2026-04-27 19:21:50 -07:00
state . is_exporting = True
try :
export_data_impl ( fp , sdk_path )
finally :
state . is_exporting = False
def export_data_impl ( fp , sdk_path ) :
2026-05-12 23:54:06 -07:00
# Reload all libraries to retrieve updated data without needing to restart Blender
for lib in bpy . data . libraries :
lib . reload ( )
log . info ( f " Reloaded: { lib . filepath } " )
2025-09-19 19:00:50 +00:00
load_external_blends ( )
2025-01-22 16:18:30 +01:00
wrd = bpy . data . worlds [ ' Lnx ' ]
rpdat = lnx . utils . get_rp ( )
if wrd . lnx_verbose_output :
print ( f ' Leenkx v { wrd . lnx_version } ( { wrd . lnx_commit } ) ' )
print ( f ' Blender: { bpy . app . version_string } , Target: { state . target } , GAPI: { lnx . utils . get_gapi ( ) } ' )
# Clean compiled variants if cache is disabled
build_dir = lnx . utils . get_fp_build ( )
if not wrd . lnx_cache_build :
if os . path . isdir ( build_dir + ' /debug/html5-resources ' ) :
shutil . rmtree ( build_dir + ' /debug/html5-resources ' , onerror = remove_readonly )
if os . path . isdir ( build_dir + ' /krom-resources ' ) :
shutil . rmtree ( build_dir + ' /krom-resources ' , onerror = remove_readonly )
if os . path . isdir ( build_dir + ' /debug/krom-resources ' ) :
shutil . rmtree ( build_dir + ' /debug/krom-resources ' , onerror = remove_readonly )
if os . path . isdir ( build_dir + ' /windows-resources ' ) :
shutil . rmtree ( build_dir + ' /windows-resources ' , onerror = remove_readonly )
if os . path . isdir ( build_dir + ' /linux-resources ' ) :
shutil . rmtree ( build_dir + ' /linux-resources ' , onerror = remove_readonly )
if os . path . isdir ( build_dir + ' /osx-resources ' ) :
shutil . rmtree ( build_dir + ' /osx-resources ' , onerror = remove_readonly )
if os . path . isdir ( build_dir + ' /compiled/Shaders ' ) :
shutil . rmtree ( build_dir + ' /compiled/Shaders ' , onerror = remove_readonly )
raw_shaders_path = sdk_path + ' /leenkx/Shaders/ '
assets_path = sdk_path + ' /leenkx/Assets/ '
export_physics = bpy . data . worlds [ ' Lnx ' ] . lnx_physics != ' Disabled '
export_navigation = bpy . data . worlds [ ' Lnx ' ] . lnx_navigation != ' Disabled '
export_ui = bpy . data . worlds [ ' Lnx ' ] . lnx_ui != ' Disabled '
export_network = bpy . data . worlds [ ' Lnx ' ] . lnx_network != ' Disabled '
assets . reset ( )
# Build node trees
LeenkxExporter . import_traits = [ ]
make_logic . build ( )
make_world . build ( )
make_renderpath . build ( )
# Export scene data
assets . embedded_data = sorted ( list ( set ( assets . embedded_data ) ) )
physics_found = False
navigation_found = False
ui_found = False
network_found = False
LeenkxExporter . compress_enabled = state . is_publish and wrd . lnx_asset_compression
LeenkxExporter . optimize_enabled = state . is_publish and wrd . lnx_optimize_data
if not os . path . exists ( build_dir + ' /compiled/Assets ' ) :
os . makedirs ( build_dir + ' /compiled/Assets ' )
# Make all 'MESH' and 'EMPTY' objects visible to the depsgraph (we pass
# this to the exporter further below) with a temporary "zoo" collection
# in the current scene. We do this to ensure that (among other things)
# modifiers are applied to all exported objects.
export_coll = bpy . data . collections . new ( " export_coll " )
bpy . context . scene . collection . children . link ( export_coll )
export_coll_names = set ( export_coll . all_objects . keys ( ) )
for scene in bpy . data . scenes :
if scene == bpy . context . scene :
continue
for o in scene . collection . all_objects :
if o . type in ( ' MESH ' , ' EMPTY ' ) :
if o . name not in export_coll_names :
export_coll . objects . link ( o )
export_coll_names . add ( o . name )
depsgraph = bpy . context . evaluated_depsgraph_get ( )
bpy . data . collections . remove ( export_coll ) # Destroy the "zoo" collection
for scene in bpy . data . scenes :
if scene . lnx_export :
2026-05-12 23:54:06 -07:00
# Reset shader comparison arrays to prevent cross-scene shader merging
assets . reset_shader_cons ( )
2025-01-22 16:18:30 +01:00
ext = ' .lz4 ' if LeenkxExporter . compress_enabled else ' .lnx '
2026-05-12 23:54:06 -07:00
asset_path = build_dir + ' /compiled/Assets/ ' + lnx . utils . safestr ( scene . name + " _ " + os . path . basename ( scene . library . filepath ) . replace ( " .blend " , " " ) if scene . library else scene . name ) + ext
2025-01-22 16:18:30 +01:00
LeenkxExporter . export_scene ( bpy . context , asset_path , scene = scene , depsgraph = depsgraph )
if LeenkxExporter . export_physics :
physics_found = True
if LeenkxExporter . export_navigation :
navigation_found = True
if LeenkxExporter . export_ui :
ui_found = True
if LeenkxExporter . export_network :
network_found = True
assets . add ( asset_path )
if physics_found is False : # Disable physics if no rigid body is exported
export_physics = False
if navigation_found is False :
export_navigation = False
if ui_found is False :
export_ui = False
if network_found == False :
export_network = False
# Ugly workaround: some logic nodes require Zui code even if no UI is used,
# for now enable UI export unless explicitly disabled.
export_ui = True
if wrd . lnx_ui == ' Disabled ' :
export_ui = False
if wrd . lnx_network == ' Enabled ' :
export_network = True
modules = [ ]
if wrd . lnx_audio == ' Enabled ' :
modules . append ( ' audio ' )
if export_physics :
modules . append ( ' physics ' )
if export_navigation :
modules . append ( ' navigation ' )
if export_ui :
modules . append ( ' ui ' )
if export_network :
modules . append ( ' network ' )
defs = lnx . utils . def_strings_to_array ( wrd . world_defs )
cdefs = lnx . utils . def_strings_to_array ( wrd . compo_defs )
if wrd . lnx_verbose_output :
log . info ( ' Exported modules: ' + ' , ' . join ( modules ) )
log . info ( ' Shader flags: ' + ' , ' . join ( defs ) )
log . info ( ' Compositor flags: ' + ' , ' . join ( cdefs ) )
log . info ( ' Khafile flags: ' + ' , ' . join ( assets . khafile_defs ) )
# Render path is configurable at runtime
has_config = wrd . lnx_write_config or os . path . exists ( lnx . utils . get_fp ( ) + ' /Bundled/config.lnx ' )
# Write compiled.inc
shaders_path = build_dir + ' /compiled/Shaders '
if not os . path . exists ( shaders_path ) :
os . makedirs ( shaders_path )
2026-04-27 19:21:50 -07:00
inc_changed = write_data . write_compiledglsl ( defs + cdefs , make_variants = has_config )
if inc_changed :
for g in glob . glob ( shaders_path + ' /*.glsl ' ) :
os . utime ( g , None )
2025-01-22 16:18:30 +01:00
# Write referenced shader passes
if not os . path . isfile ( build_dir + ' /compiled/Shaders/shader_datas.lnx ' ) or state . last_world_defs != wrd . world_defs :
res = { ' shader_datas ' : [ ] }
for ref in assets . shader_passes :
# Ensure shader pass source exists
if not os . path . exists ( raw_shaders_path + ' / ' + ref ) :
continue
assets . shader_passes_assets [ ref ] = [ ]
compile_shader_pass ( res , raw_shaders_path , ref , defs + cdefs , make_variants = has_config )
# Workaround to also export non-material world shaders
res [ ' shader_datas ' ] + = make_world . shader_datas
if rpdat . lnx_lens or rpdat . lnx_lut :
for shader_pass in res [ " shader_datas " ] :
for context in shader_pass [ " contexts " ] :
for texture_unit in context [ " texture_units " ] :
# Lens Texture
if rpdat . lnx_lens_texture != ' ' and rpdat . lnx_lens_texture != ' lenstexture.jpg ' and " link " in texture_unit and texture_unit [ " link " ] == " $lenstexture.jpg " :
texture_unit [ " link " ] = f " $ { rpdat . lnx_lens_texture } "
# LUT Colorgrading
if rpdat . lnx_lut_texture != ' ' and rpdat . lnx_lut_texture != ' luttexture.jpg ' and " link " in texture_unit and texture_unit [ " link " ] == " $luttexture.jpg " :
texture_unit [ " link " ] = f " $ { rpdat . lnx_lut_texture } "
lnx . utils . write_lnx ( shaders_path + ' /shader_datas.lnx ' , res )
if wrd . lnx_debug_console and rpdat . rp_renderer == ' Deferred ' :
# Copy deferred shader so that it can include compiled.inc
line_deferred_src = os . path . join ( sdk_path , ' leenkx ' , ' Shaders ' , ' debug_draw ' , ' line_deferred.frag.glsl ' )
line_deferred_dst = os . path . join ( shaders_path , ' line_deferred.frag.glsl ' )
shutil . copyfile ( line_deferred_src , line_deferred_dst )
for ref in assets . shader_passes :
for s in assets . shader_passes_assets [ ref ] :
assets . add_shader ( shaders_path + ' / ' + s + ' .glsl ' )
for file in assets . shaders_external :
name = file . split ( ' / ' ) [ - 1 ] . split ( ' \\ ' ) [ - 1 ]
target = build_dir + ' /compiled/Shaders/ ' + name
if not os . path . exists ( target ) :
shutil . copy ( file , target )
state . last_world_defs = wrd . world_defs
# Reset path
os . chdir ( fp )
# Copy std shaders
if not os . path . isdir ( build_dir + ' /compiled/Shaders/std ' ) :
shutil . copytree ( raw_shaders_path + ' std ' , build_dir + ' /compiled/Shaders/std ' )
# Write config.lnx
resx , resy = lnx . utils . get_render_resolution ( lnx . utils . get_active_scene ( ) )
if wrd . lnx_write_config :
write_data . write_config ( resx , resy )
# Change project version (Build, Publish)
if ( not state . is_play ) and ( wrd . lnx_project_version_autoinc ) :
wrd . lnx_project_version = lnx . utils . change_version_project ( wrd . lnx_project_version )
# Write khafile.js
write_data . write_khafilejs ( state . is_play , export_physics , export_navigation , export_ui , export_network , state . is_publish , LeenkxExporter . import_traits )
# Write Main.hx - depends on write_khafilejs for writing number of assets
scene_name = lnx . utils . get_project_scene_name ( )
write_data . write_mainhx ( scene_name , resx , resy , state . is_play , state . is_publish )
if scene_name != state . last_scene or resx != state . last_resx or resy != state . last_resy :
wrd . lnx_recompile = True
state . last_resx = resx
state . last_resy = resy
state . last_scene = scene_name
2025-09-19 19:00:50 +00:00
clear_external_scenes ( )
2025-01-22 16:18:30 +01:00
def compile ( assets_only = False ) :
wrd = bpy . data . worlds [ ' Lnx ' ]
fp = lnx . utils . get_fp ( )
os . chdir ( fp )
node_path = lnx . utils . get_node_path ( )
khamake_path = lnx . utils . get_khamake_path ( )
cmd = [ node_path , khamake_path ]
# Custom exporter
if state . target == " custom " :
if len ( wrd . lnx_exporterlist ) > 0 :
item = wrd . lnx_exporterlist [ wrd . lnx_exporterlist_index ]
if item . lnx_project_target == ' custom ' and item . lnx_project_khamake != ' ' :
for s in item . lnx_project_khamake . split ( ' ' ) :
cmd . append ( s )
state . proc_build = run_proc ( cmd , build_done )
else :
target_name = state . target
kha_target_name = lnx . utils . get_kha_target ( target_name )
if kha_target_name != ' ' :
cmd . append ( kha_target_name )
ffmpeg_path = lnx . utils . get_ffmpeg_path ( )
if ffmpeg_path not in ( None , ' ' ) :
cmd . append ( ' --ffmpeg ' )
cmd . append ( ffmpeg_path ) # '"' + ffmpeg_path + '"'
state . export_gapi = lnx . utils . get_gapi ( )
cmd . append ( ' -g ' )
cmd . append ( state . export_gapi )
# Windows - Set Visual Studio Version
if state . target . startswith ( ' windows ' ) :
cmd . append ( ' --visualstudio ' )
cmd . append ( lnx . utils_vs . version_to_khamake_id [ wrd . lnx_project_win_list_vs ] )
if lnx . utils . get_legacy_shaders ( ) or ' ios ' in state . target :
if ' html5 ' in state . target or ' ios ' in state . target :
pass
else :
cmd . append ( ' --shaderversion ' )
cmd . append ( ' 110 ' )
elif ' android ' in state . target or ' html5 ' in state . target :
cmd . append ( ' --shaderversion ' )
cmd . append ( ' 300 ' )
else :
cmd . append ( ' --shaderversion ' )
cmd . append ( ' 330 ' )
if ' _VR ' in wrd . world_defs :
cmd . append ( ' --vr ' )
cmd . append ( ' webvr ' )
if lnx . utils . get_pref_or_default ( ' khamake_debug ' , False ) :
cmd . append ( ' --debug ' )
if lnx . utils . get_rp ( ) . rp_renderer == ' Raytracer ' :
cmd . append ( ' --raytrace ' )
cmd . append ( ' dxr ' )
dxc_path = fp + ' /HlslShaders/dxc.exe '
subprocess . Popen ( [ dxc_path , ' -Zpr ' , ' -Fo ' , fp + ' /Bundled/raytrace.cso ' , ' -T ' , ' lib_6_3 ' , fp + ' /HlslShaders/raytrace.hlsl ' ] ) . wait ( )
if lnx . utils . get_khamake_threads ( ) != 1 :
cmd . append ( ' --parallelAssetConversion ' )
cmd . append ( str ( lnx . utils . get_khamake_threads ( ) ) )
compilation_server = False
cmd . append ( ' --to ' )
if ( kha_target_name == ' krom ' and not state . is_publish ) or ( kha_target_name == ' html5 ' and not state . is_publish ) :
cmd . append ( lnx . utils . build_dir ( ) + ' /debug ' )
# Start compilation server
if kha_target_name == ' krom ' and lnx . utils . get_compilation_server ( ) and not assets_only and wrd . lnx_cache_build :
compilation_server = True
lnx . lib . server . run_haxe ( lnx . utils . get_haxe_path ( ) )
else :
cmd . append ( lnx . utils . build_dir ( ) )
if not wrd . lnx_verbose_output :
cmd . append ( " --quiet " )
#Project needs to be compiled at least once
#before compilation server can work
if not os . path . exists ( lnx . utils . build_dir ( ) + ' /debug/krom/krom.js ' ) and not state . is_publish :
state . proc_build = run_proc ( cmd , build_done )
else :
if assets_only or compilation_server :
cmd . append ( ' --nohaxe ' )
cmd . append ( ' --noproject ' )
if len ( wrd . lnx_exporterlist ) > 0 :
item = wrd . lnx_exporterlist [ wrd . lnx_exporterlist_index ]
if item . lnx_project_khamake != " " :
for s in item . lnx_project_khamake . split ( " " ) :
cmd . append ( s )
state . proc_build = run_proc ( cmd , assets_done if compilation_server else build_done )
if bpy . app . background :
if state . proc_build . returncode == 0 :
build_success ( )
else :
log . error ( ' Build failed ' )
def build ( target , is_play = False , is_publish = False , is_export = False ) :
global profile_time
profile_time = time . time ( )
2025-04-09 20:33:24 +00:00
wrd = bpy . data . worlds [ ' Lnx ' ]
if is_play and wrd . lnx_runtime == ' Hashlink ' :
current_os = lnx . utils . get_os ( )
if current_os == ' win ' :
target = ' windows-hl '
elif current_os == ' linux ' :
target = ' linux-hl '
elif current_os == ' macos ' :
target = ' macos-hl '
else :
log . error ( f " Unsupported OS ' { current_os } ' for Hashlink runtime. " )
2025-01-22 16:18:30 +01:00
state . target = target
state . is_play = is_play
state . is_publish = is_publish
state . is_export = is_export
# Save blend
if lnx . utils . get_save_on_build ( ) :
bpy . ops . wm . save_mainfile ( )
log . clear ( clear_warnings = True , clear_errors = True )
# Set camera in active scene
active_scene = lnx . utils . get_active_scene ( )
if active_scene . camera == None :
for o in active_scene . objects :
if o . type == ' CAMERA ' :
active_scene . camera = o
break
# Get paths
sdk_path = lnx . utils . get_sdk_path ( )
raw_shaders_path = sdk_path + ' /leenkx/Shaders/ '
# Set dir
fp = lnx . utils . get_fp ( )
os . chdir ( fp )
# Create directories
wrd = bpy . data . worlds [ ' Lnx ' ]
sources_path = ' Sources/ ' + lnx . utils . safestr ( wrd . lnx_project_package )
if not os . path . exists ( sources_path ) :
os . makedirs ( sources_path )
# Save external scripts edited inside Blender
write_texts = False
for text in bpy . data . texts :
if text . filepath != ' ' and text . is_dirty :
write_texts = True
break
if write_texts :
area = bpy . context . area
if area is not None :
old_type = area . type
area . type = ' TEXT_EDITOR '
for text in bpy . data . texts :
if text . filepath != ' ' and text . is_dirty and os . path . isfile ( text . filepath ) :
area . spaces [ 0 ] . text = text
bpy . ops . text . save ( )
area . type = old_type
# Save internal Haxe scripts
for text in bpy . data . texts :
if text . filepath == ' ' and text . name [ - 3 : ] == ' .hx ' :
with open ( ' Sources/ ' + lnx . utils . safestr ( wrd . lnx_project_package ) + ' / ' + text . name , ' w ' , encoding = ' utf-8 ' ) as f :
f . write ( text . as_string ( ) )
# Export data
export_data ( fp , sdk_path )
if state . target == ' html5 ' :
w , h = lnx . utils . get_render_resolution ( lnx . utils . get_active_scene ( ) )
write_data . write_indexhtml ( w , h , is_publish )
# Bundle files from include dir
if os . path . isdir ( ' include ' ) :
dest = ' /html5/ ' if is_publish else ' /debug/html5/ '
for fn in glob . iglob ( os . path . join ( ' include ' , ' ** ' ) , recursive = False ) :
shutil . copy ( fn , lnx . utils . build_dir ( ) + dest + os . path . basename ( fn ) )
def play_done ( ) :
""" Called if the player was stopped/terminated. """
if state . proc_play is not None :
if state . proc_play . returncode != 0 :
log . warn ( f ' Player exited code { state . proc_play . returncode } ' )
state . proc_play = None
state . redraw_ui = True
log . clear ( )
live_patch . stop ( )
2026-02-24 17:35:26 -08:00
_viewport_processes = { }
def _generate_viewport_id ( space_data = None ) :
""" Generate a unique viewport ID from space_data pointer or random. """
if space_data is not None :
try :
return hex ( space_data . as_pointer ( ) ) [ - 6 : ]
except :
pass
import random
return hex ( random . randint ( 0 , 0xFFFFFF ) ) [ 2 : ] . zfill ( 6 )
def _get_viewport_shmem_name ( viewport_id ) :
""" Get shared memory name for a specific viewport. """
return f " KROM_VIEWPORT_FB_ { viewport_id } "
def _kill_viewport_process ( viewport_id ) :
""" Kill a specific viewport ' s Krom process. """
global _viewport_processes
if viewport_id in _viewport_processes :
proc = _viewport_processes [ viewport_id ]
try :
proc . terminate ( )
proc . wait ( timeout = 2 )
except :
try :
proc . kill ( )
except :
pass
del _viewport_processes [ viewport_id ]
def _kill_all_viewport_processes ( ) :
""" Kill all viewport Krom processes. """
global _viewport_processes
for viewport_id in list ( _viewport_processes . keys ( ) ) :
_kill_viewport_process ( viewport_id )
2026-04-27 19:21:50 -07:00
2026-02-24 17:35:26 -08:00
if lnx . utils . get_os ( ) == ' win ' :
try :
result = subprocess . run (
[ ' tasklist ' , ' /FI ' , ' IMAGENAME eq Krom.exe ' , ' /FO ' , ' CSV ' , ' /NH ' ] ,
capture_output = True , text = True , timeout = 5
)
if ' Krom.exe ' in result . stdout :
subprocess . run ( [ ' taskkill ' , ' /F ' , ' /IM ' , ' Krom.exe ' ] ,
capture_output = True , timeout = 5 )
import time
time . sleep ( 0.3 )
except :
pass
def run_viewport_runtime ( viewport_id , width = 1920 , height = 1080 ) :
""" Launch a viewport which gets its own Krom process with unique shared memory. """
global _viewport_processes
2026-04-27 19:21:50 -07:00
2026-02-24 17:35:26 -08:00
if ' Lnx ' not in bpy . data . worlds :
log . warn ( ' No Lnx world found - cannot start viewport server ' )
return None , None
2026-04-27 19:21:50 -07:00
2026-02-24 17:35:26 -08:00
_kill_viewport_process ( viewport_id )
2026-04-27 19:21:50 -07:00
2026-02-24 17:35:26 -08:00
shmem_name = _get_viewport_shmem_name ( viewport_id )
wrd = bpy . data . worlds [ ' Lnx ' ]
krom_location , krom_path = lnx . utils . krom_paths ( )
path = lnx . utils . get_fp_build ( ) + ' /debug/krom '
path_resources = path + ' -resources '
2026-04-27 19:21:50 -07:00
2026-02-24 17:35:26 -08:00
if not os . path . exists ( path + ' /krom.js ' ) :
log . warn ( f ' Krom build not found at { path } /krom.js - build project first ' )
return None , None
os . chdir ( krom_location )
cmd = [ krom_path , path , path_resources ]
if lnx . utils . get_os ( ) == ' win ' :
cmd . append ( ' --consolepid ' )
cmd . append ( str ( os . getpid ( ) ) )
if wrd . lnx_audio == ' Disabled ' :
cmd . append ( ' --nosound ' )
2026-04-27 19:21:50 -07:00
2026-02-24 17:35:26 -08:00
cmd . append ( ' --viewport-server ' )
cmd . append ( ' --shmem ' )
cmd . append ( shmem_name )
cmd . append ( ' --viewport-width ' )
cmd . append ( str ( width ) )
cmd . append ( ' --viewport-height ' )
cmd . append ( str ( height ) )
try :
proc = subprocess . Popen ( cmd )
_viewport_processes [ viewport_id ] = proc
log . info ( f ' Started: { viewport_id } (shmem= { shmem_name } ) ' )
return proc , shmem_name
except Exception as e :
log . error ( f ' Failed to start viewport runtime: { e } ' )
return None , None
def stop_viewport_runtime ( viewport_id ) :
""" Stop a specific viewport ' s Krom process. """
_kill_viewport_process ( viewport_id )
_viewport_build_in_progress = False
_viewport_pending_launches = [ ] # List of (viewport_id, width, height) tuples
_viewport_proc_build = None # Separate process tracker for viewport builds
def build_viewport ( viewport_id , width = 1920 , height = 1080 ) :
""" Build project for viewport """
global _viewport_build_in_progress , _viewport_pending_launches , profile_time
wrd = bpy . data . worlds . get ( ' Lnx ' )
if not wrd :
log . warn ( ' No Lnx world found - cannot build for viewport ' )
return
2026-04-27 19:21:50 -07:00
2026-02-24 17:35:26 -08:00
krom_js_path = lnx . utils . get_fp_build ( ) + ' /debug/krom/krom.js '
if os . path . exists ( krom_js_path ) and not wrd . lnx_recompile :
log . info ( f ' Using cached viewport build for { viewport_id } ' )
run_viewport_runtime ( viewport_id , width , height )
return
2026-04-27 19:21:50 -07:00
2026-02-24 17:35:26 -08:00
pending_entry = ( viewport_id , width , height )
if pending_entry not in _viewport_pending_launches :
_viewport_pending_launches . append ( pending_entry )
log . info ( f ' Queued viewport { viewport_id } for launch after build ' )
# If a build is already in progress and there is an actual build process, wait
# _viewport_proc_build checks viewport ,not state.proc_build (which is for play button)
if _viewport_build_in_progress :
if _viewport_proc_build is not None and _viewport_proc_build . poll ( ) is None :
2026-04-27 19:21:50 -07:00
log . info ( f ' Build in progress, viewport { viewport_id } will launch when ready ' )
2026-02-24 17:35:26 -08:00
return
else :
log . info ( f ' Resetting stale viewport build state ' )
_viewport_build_in_progress = False
_viewport_build_in_progress = True
profile_time = time . time ( )
log . info ( f ' Starting viewport build for { len ( _viewport_pending_launches ) } viewport(s) ' )
# Set viewport mode flags but not the is_play flag meant for external launcher
state . is_viewport = True
state . viewport_width = width
state . viewport_height = height
state . target = ' krom '
state . is_play = False # NOT play mode
state . is_publish = False
state . is_export = False
2026-04-27 19:21:50 -07:00
2026-02-24 17:35:26 -08:00
log . clear ( clear_warnings = True , clear_errors = True )
sdk_path = lnx . utils . get_sdk_path ( )
fp = lnx . utils . get_fp ( )
os . chdir ( fp )
2026-04-27 19:21:50 -07:00
2026-02-24 17:35:26 -08:00
sources_path = ' Sources/ ' + lnx . utils . safestr ( wrd . lnx_project_package )
if not os . path . exists ( sources_path ) :
os . makedirs ( sources_path )
2026-04-27 19:21:50 -07:00
2026-02-24 17:35:26 -08:00
log . info ( ' Exporting scene data... ' )
export_data ( fp , sdk_path )
2026-04-27 19:21:50 -07:00
2026-02-24 17:35:26 -08:00
log . info ( ' Starting Krom compilation for viewport... ' )
compile_viewport ( assets_only = ( not wrd . lnx_recompile ) )
def compile_viewport ( assets_only = False ) :
""" Compile for viewport mode using separate process tracking. """
global _viewport_proc_build
wrd = bpy . data . worlds [ ' Lnx ' ]
fp = lnx . utils . get_fp ( )
os . chdir ( fp )
node_path = lnx . utils . get_node_path ( )
khamake_path = lnx . utils . get_khamake_path ( )
cmd = [ node_path , khamake_path , ' krom ' ]
ffmpeg_path = lnx . utils . get_ffmpeg_path ( )
if ffmpeg_path not in ( None , ' ' ) :
cmd . append ( ' --ffmpeg ' )
cmd . append ( ffmpeg_path )
cmd . append ( ' -g ' )
cmd . append ( lnx . utils . get_gapi ( ) )
cmd . append ( ' --shaderversion ' )
cmd . append ( ' 330 ' )
if lnx . utils . get_khamake_threads ( ) != 1 :
cmd . append ( ' --parallelAssetConversion ' )
cmd . append ( str ( lnx . utils . get_khamake_threads ( ) ) )
cmd . append ( ' --to ' )
cmd . append ( lnx . utils . build_dir ( ) + ' /debug ' )
if not wrd . lnx_verbose_output :
cmd . append ( " --quiet " )
if assets_only :
cmd . append ( ' --nohaxe ' )
cmd . append ( ' --noproject ' )
log . info ( f ' Running: { " " . join ( cmd ) } ' )
_viewport_proc_build = run_proc ( cmd , viewport_build_done )
def viewport_build_done ( ) :
""" Called when viewport build completes - launches all pending Krom processes. """
global _viewport_build_in_progress , _viewport_pending_launches , _viewport_proc_build
log . info ( ' Viewport compilation finished ' )
if _viewport_proc_build is None :
_viewport_build_in_progress = False
return
result = _viewport_proc_build . poll ( )
_viewport_proc_build = None
_viewport_build_in_progress = False
state . redraw_ui = True
if result == 0 :
bpy . data . worlds [ ' Lnx ' ] . lnx_recompile = False
2026-04-27 19:21:50 -07:00
2026-02-24 17:35:26 -08:00
if _viewport_pending_launches :
pending = _viewport_pending_launches . copy ( )
_viewport_pending_launches . clear ( )
log . info ( f ' Launching { len ( pending ) } Krom instance(s) ' )
for viewport_id , width , height in pending :
run_viewport_runtime ( viewport_id , width , height )
else :
log . info ( ' No pending viewports to launch ' )
else :
_viewport_pending_launches . clear ( )
log . error ( ' Viewport build failed, check console ' )
def play_viewport ( viewport_id , width = 1920 , height = 1080 ) :
""" Launch Krom in a viewport """
global _viewport_build_in_progress , _viewport_pending_launches , _viewport_proc_build
if not viewport_id :
2026-04-27 19:21:50 -07:00
log . error ( ' No viewport_id: play_viewport requires an id ' )
2026-02-24 17:35:26 -08:00
return
2026-04-27 19:21:50 -07:00
2026-02-24 17:35:26 -08:00
if ' Lnx ' not in bpy . data . worlds :
2026-04-27 19:21:50 -07:00
log . error ( ' No Lnx world found - cannot start viewport server ' )
2026-02-24 17:35:26 -08:00
return
wrd = bpy . data . worlds [ ' Lnx ' ]
2026-04-27 19:21:50 -07:00
2026-02-24 17:35:26 -08:00
krom_js_path = lnx . utils . get_fp_build ( ) + ' /debug/krom/krom.js '
if os . path . exists ( krom_js_path ) and not wrd . lnx_recompile :
run_viewport_runtime ( viewport_id , width , height )
return
2026-04-27 19:21:50 -07:00
2026-02-24 17:35:26 -08:00
pending_entry = ( viewport_id , width , height )
if pending_entry not in _viewport_pending_launches :
_viewport_pending_launches . append ( pending_entry )
if _viewport_build_in_progress :
if _viewport_proc_build is not None and _viewport_proc_build . poll ( ) is None :
log . info ( f ' Build in progress, viewport { viewport_id } will launch when ready ' )
return
else :
_viewport_build_in_progress = False # Reset stale state
2026-04-27 19:21:50 -07:00
2026-02-24 17:35:26 -08:00
_viewport_build_in_progress = True
sdk_path = lnx . utils . get_sdk_path ( )
fp = lnx . utils . get_fp ( )
os . chdir ( fp )
2026-04-27 19:21:50 -07:00
2026-02-24 17:35:26 -08:00
sources_path = ' Sources/ ' + lnx . utils . safestr ( wrd . lnx_project_package )
if not os . path . exists ( sources_path ) :
os . makedirs ( sources_path )
# export data same as play() but without setting state.is_play
state . target = ' krom '
state . is_publish = False
state . is_export = False
export_data ( fp , sdk_path )
2026-04-27 19:21:50 -07:00
2026-02-24 17:35:26 -08:00
compile_viewport ( assets_only = ( not wrd . lnx_recompile ) )
def stop_viewport ( viewport_id = None ) :
""" Stop a specific viewport or all viewports. """
if viewport_id :
_kill_viewport_process ( viewport_id )
else :
_kill_all_viewport_processes ( )
2025-01-22 16:18:30 +01:00
def assets_done ( ) :
if state . proc_build == None :
return
result = state . proc_build . poll ( )
if result == 0 :
# Connect to the compilation server
os . chdir ( lnx . utils . build_dir ( ) + ' /debug/ ' )
cmd = [ lnx . utils . get_haxe_path ( ) , ' --connect ' , ' 6000 ' , ' project-krom.hxml ' ]
state . proc_build = run_proc ( cmd , compilation_server_done )
else :
state . proc_build = None
state . redraw_ui = True
log . error ( ' Build failed, check console ' )
def compilation_server_done ( ) :
if state . proc_build == None :
return
result = state . proc_build . poll ( )
if result == 0 :
if os . path . exists ( ' krom/krom.js.temp ' ) :
os . chmod ( ' krom/krom.js ' , stat . S_IWRITE )
os . remove ( ' krom/krom.js ' )
os . rename ( ' krom/krom.js.temp ' , ' krom/krom.js ' )
build_done ( )
else :
state . proc_build = None
state . redraw_ui = True
log . error ( ' Build failed, check console ' )
def build_done ( ) :
wrd = bpy . data . worlds [ ' Lnx ' ]
log . info ( ' Finished in {:0.3f} s ' . format ( time . time ( ) - profile_time ) )
if log . num_warnings > 0 :
log . print_warn ( f ' { log . num_warnings } warning { " s " if log . num_warnings > 1 else " " } occurred during compilation ' )
if state . proc_build is None :
return
result = state . proc_build . poll ( )
state . proc_build = None
state . redraw_ui = True
if result == 0 :
bpy . data . worlds [ ' Lnx ' ] . lnx_recompile = False
build_success ( )
else :
log . error ( ' Build failed, check console ' )
def runtime_to_target ( ) :
wrd = bpy . data . worlds [ ' Lnx ' ]
if wrd . lnx_runtime == ' Krom ' :
return ' krom '
return ' html5 '
def get_khajs_path ( target ) :
if target == ' krom ' :
return lnx . utils . build_dir ( ) + ' /debug/krom/krom.js '
return lnx . utils . build_dir ( ) + ' /debug/html5/kha.js '
def play ( ) :
global scripts_mtime
wrd = bpy . data . worlds [ ' Lnx ' ]
build ( target = runtime_to_target ( ) , is_play = True )
khajs_path = get_khajs_path ( state . target )
if not wrd . lnx_cache_build or \
not os . path . isfile ( khajs_path ) or \
assets . khafile_defs_last != assets . khafile_defs or \
state . last_target != state . target :
wrd . lnx_recompile = True
state . last_target = state . target
# Trait sources modified
state . mod_scripts = [ ]
script_path = lnx . utils . get_fp ( ) + ' /Sources/ ' + lnx . utils . safestr ( wrd . lnx_project_package )
if os . path . isdir ( script_path ) :
new_mtime = scripts_mtime
for fn in glob . iglob ( os . path . join ( script_path , ' ** ' , ' *.hx ' ) , recursive = True ) :
mtime = os . path . getmtime ( fn )
if scripts_mtime < mtime :
lnx . utils . fetch_script_props ( fn ) # Trait props
fn = fn . split ( ' Sources/ ' ) [ 1 ]
fn = fn [ : - 3 ] #.hx
fn = fn . replace ( ' / ' , ' . ' )
state . mod_scripts . append ( fn )
wrd . lnx_recompile = True
if new_mtime < mtime :
new_mtime = mtime
scripts_mtime = new_mtime
if len ( state . mod_scripts ) > 0 : # Trait props
lnx . utils . fetch_trait_props ( )
compile ( assets_only = ( not wrd . lnx_recompile ) )
def build_success ( ) :
log . clear ( )
wrd = bpy . data . worlds [ ' Lnx ' ]
if state . is_play :
cmd = [ ]
width , height = lnx . utils . get_render_resolution ( lnx . utils . get_active_scene ( ) )
if wrd . lnx_runtime == ' Browser ' :
os . chdir ( lnx . utils . get_fp ( ) )
prefs = lnx . utils . get_lnx_preferences ( )
build_dir = lnx . utils . build_dir ( )
path = ' {} /debug/html5/ ' . format ( build_dir )
browser = webbrowser . get ( )
browsername = None
if hasattr ( browser , " name " ) :
browsername = getattr ( browser , ' name ' )
elif hasattr ( browser , " _name " ) :
browsername = getattr ( browser , ' _name ' )
envvar = ' LEENKX_PLAY_HTML5 '
if envvar in os . environ :
envcmd = os . environ [ envvar ]
if len ( envcmd ) == 0 :
log . warn ( f " Your { envvar } environment variable is set to an empty string " )
else :
2026-04-27 19:21:50 -07:00
host = ' localhost '
t = threading . Thread ( name = ' localserver ' ,
target = lnx . lib . server . run_tcp ,
args = ( prefs . html5_server_port ,
prefs . html5_server_log ) ,
daemon = True )
t . start ( )
url = ' http:// {} : {} / {} ' . format ( host , prefs . html5_server_port , path )
2025-01-22 16:18:30 +01:00
tplstr = Template ( envcmd ) . safe_substitute ( {
' host ' : host ,
' port ' : prefs . html5_server_port ,
' width ' : width ,
' height ' : height ,
' url ' : url ,
' path ' : path ,
' dir ' : build_dir ,
' browser ' : browsername
} )
cmd = re . split ( ' + ' , tplstr )
if len ( cmd ) == 0 :
2026-04-27 19:21:50 -07:00
# try file:// protocol with a Chromium-based browser
fast = browsername if browsername and any ( b in browsername . lower ( ) for b in ( ' chrome ' , ' chromium ' , ' edge ' , ' msedge ' ) ) else lnx . utils . find_browser ( )
if fast is not None :
file_url = ' file:/// ' + os . path . abspath ( path + ' index.html ' ) . replace ( ' \\ ' , ' / ' )
subprocess . Popen ( [ fast , ' --allow-file-access-from-files ' , ' --no-first-run ' ,
' --user-data-dir= ' + os . path . join ( tempfile . gettempdir ( ) , ' leenkx_browser ' ) ,
file_url ] )
return
host = ' localhost '
t = threading . Thread ( name = ' localserver ' ,
target = lnx . lib . server . run_tcp ,
args = ( prefs . html5_server_port ,
prefs . html5_server_log ) ,
daemon = True )
t . start ( )
url = ' http:// {} : {} / {} ' . format ( host , prefs . html5_server_port , path )
2025-01-22 16:18:30 +01:00
if browsername in ( None , ' ' , ' default ' ) :
webbrowser . open ( url )
return
2026-04-27 19:21:50 -07:00
else :
cmd = [ browsername , url ]
2025-01-22 16:18:30 +01:00
elif wrd . lnx_runtime == ' Krom ' :
if wrd . lnx_live_patch :
live_patch . start ( )
open ( lnx . utils . get_fp_build ( ) + ' /debug/krom/krom.patch ' , ' w ' , encoding = ' utf-8 ' ) . close ( )
krom_location , krom_path = lnx . utils . krom_paths ( )
path = lnx . utils . get_fp_build ( ) + ' /debug/krom '
path_resources = path + ' -resources '
pid = os . getpid ( )
os . chdir ( krom_location )
envvar = ' LEENKX_PLAY_KROM '
if envvar in os . environ :
envcmd = os . environ [ envvar ]
if len ( envcmd ) == 0 :
log . warn ( f " Your { envvar } environment variable is set to an empty string " )
else :
tplstr = Template ( envcmd ) . safe_substitute ( {
' pid ' : pid ,
' audio ' : wrd . lnx_audio != ' Disabled ' ,
' location ' : krom_location ,
' krom_path ' : krom_path ,
' path ' : path ,
' resources ' : path_resources ,
' width ' : width ,
' height ' : height
} )
cmd = re . split ( ' + ' , tplstr )
if len ( cmd ) == 0 :
cmd = [ krom_path , path , path_resources ]
if lnx . utils . get_os ( ) == ' win ' :
cmd . append ( ' --consolepid ' )
cmd . append ( str ( pid ) )
if wrd . lnx_audio == ' Disabled ' :
cmd . append ( ' --nosound ' )
2026-02-24 17:35:26 -08:00
if state . is_viewport :
cmd . append ( ' --viewport-server ' )
cmd . append ( ' --shmem ' )
if state . viewport_id :
cmd . append ( f ' KROM_VIEWPORT_FB_ { state . viewport_id } ' )
else :
cmd . append ( ' KROM_VIEWPORT_FB ' )
cmd . append ( ' --viewport-width ' )
cmd . append ( str ( state . viewport_width ) )
cmd . append ( ' --viewport-height ' )
cmd . append ( str ( state . viewport_height ) )
2025-04-09 20:33:24 +00:00
2025-04-10 10:12:22 +00:00
elif state . target . startswith ( ( ' windows-hl ' , ' linux-hl ' , ' macos-hl ' ) ) :
log . info ( f " Runtime Hashlink/C target: { state . target } " )
2025-04-09 20:33:24 +00:00
hl_build_dir , _ , _ = lnx . utils . hashlink_paths ( state . target )
if not hl_build_dir :
log . error ( f " Could not find build directory for target { state . target } . Playback aborted. " )
return
if state . target == ' windows-hl ' :
vs_version_major = wrd . lnx_project_win_list_vs
build_mode = wrd . lnx_project_win_build_mode # Debug or Release
build_arch = wrd . lnx_project_win_build_arch # x64 or x86 (maps to Win32 for MSBuild)
platform = ' x64 ' if build_arch == ' x64 ' else ' Win32 ' # MSBuild uses Win32 for x86
installation = lnx . utils_vs . get_installed_version ( vs_version_major , re_fetch = True )
if installation is None :
vs_info = lnx . utils_vs . get_supported_version ( vs_version_major )
log . error ( f ' Visual Studio { vs_info [ " name " ] } not found. Cannot compile { state . target } . ' )
return
msbuild_path = os . path . join ( installation [ ' path ' ] , ' MSBuild ' , ' Current ' , ' Bin ' , ' MSBuild.exe ' )
if not os . path . isfile ( msbuild_path ) :
msbuild_path = os . path . join ( installation [ ' path ' ] , ' MSBuild ' , ' 15.0 ' , ' Bin ' , ' MSBuild.exe ' ) # VS 2017 fallback
if not os . path . isfile ( msbuild_path ) :
log . error ( f ' MSBuild.exe not found for { installation [ " name " ] } . Cannot compile { state . target } . ' )
return
proj_name = lnx . utils . blend_name ( )
vcxproj_path = os . path . join ( hl_build_dir , proj_name + ' .vcxproj ' )
if not os . path . isfile ( vcxproj_path ) :
found_vcxproj = None
for file in os . listdir ( hl_build_dir ) :
if file . endswith ( " .vcxproj " ) :
found_vcxproj = os . path . join ( hl_build_dir , file )
log . warn ( f " Could not find ' { proj_name } .vcxproj ' , using found ' { file } ' instead. " )
break
if not found_vcxproj :
log . error ( f ' .vcxproj file not found in { hl_build_dir } . Cannot compile. ' )
return
vcxproj_path = found_vcxproj
proj_name = os . path . splitext ( os . path . basename ( vcxproj_path ) ) [ 0 ]
msbuild_cmd = [
msbuild_path ,
vcxproj_path ,
f ' /p:Configuration= { build_mode } ' ,
f ' /p:Platform= { platform } ' ,
' /m '
]
log . info ( f " Compiling { state . target } project with MSBuild... " )
log . info ( f " Command: { ' ' . join ( msbuild_cmd ) } " )
compile_success = False
try :
compile_result = subprocess . run ( msbuild_cmd , cwd = hl_build_dir , check = False , capture_output = True , text = True )
if compile_result . returncode == 0 :
log . info ( f " MSBuild compilation successful. " )
compile_success = True
else :
log . error ( f " MSBuild compilation failed (Exit Code: { compile_result . returncode } ). " )
log . error ( f " MSBuild Output: \n { compile_result . stdout } " )
log . error ( f " MSBuild Errors: \n { compile_result . stderr } " )
except Exception as e :
log . error ( f " Error running MSBuild: { e } " )
traceback . print_exc ( )
if not compile_success :
return
exe_path = os . path . join ( hl_build_dir , platform , build_mode , proj_name + ' .exe ' )
if not os . path . isfile ( exe_path ) :
exe_path = os . path . join ( hl_build_dir , build_mode , proj_name + ' .exe ' )
if not os . path . isfile ( exe_path ) :
log . error ( f ' Compiled executable not found at expected location: { exe_path } (or variants). Cannot run. ' )
return
log . info ( f " Found compiled executable: { exe_path } " )
2025-04-10 10:12:22 +00:00
dest_exe_name = proj_name + ' .exe '
base_build_dir = lnx . utils . get_fp_build ( )
dest_dir = os . path . join ( base_build_dir , state . target )
dest_path = os . path . join ( dest_dir , dest_exe_name )
try :
shutil . move ( exe_path , dest_path )
cmd = [ dest_path ]
except Exception as e :
cmd = [ exe_path ]
os . chdir ( dest_dir )
2025-04-09 20:33:24 +00:00
elif state . target in ( ' linux-hl ' , ' macos-hl ' ) :
2025-04-10 15:54:53 +00:00
wrd = bpy . data . worlds [ ' Lnx ' ]
paths = lnx . utils . hashlink_paths ( state . target )
hl_build_dir = paths [ 0 ]
# TO DO switch from default Release
build_mode = ' Release '
proj_name = lnx . utils . blend_name ( )
exe_path = str ( hl_build_dir + " / " + build_mode )
if not exe_path :
log . error ( f " Build finished, but could not find the executable for { state . target } . " )
return
makefile_path = os . path . join ( exe_path , ' makefile ' )
if not os . path . isfile ( makefile_path ) :
log . error ( f " Makefile not found at ' { makefile_path } ' . Cannot compile C code. " )
return
make_cmd = [ ' make ' ]
log . info ( f " Compiling C code using ' make ' in directory ' { exe_path } ' ... " )
log . info ( f " Make command: { ' ' . join ( make_cmd ) } " )
try :
result = subprocess . run ( make_cmd , cwd = exe_path , check = True , capture_output = True , text = True , encoding = ' utf-8 ' )
log . info ( " ' make ' compilation successful. " )
except subprocess . CalledProcessError as e :
log . error ( f " ' make ' compilation failed with return code { e . returncode } . " )
log . error ( f " Make Error Output: \n { e . stderr } " )
return
except FileNotFoundError :
log . error ( " ' make ' command not found. Ensure ' make ' is installed and in your system ' s PATH. " )
return
except Exception as e :
log . error ( f " An unexpected error occurred running make: { e } " )
return
2025-04-09 20:33:24 +00:00
2025-04-10 15:54:53 +00:00
log . info ( f " Found compiled executable: { exe_path } " )
dest_exe_name = lnx . utils . safesrc ( wrd . lnx_project_name + ' - ' + wrd . lnx_project_version )
base_build_dir = lnx . utils . get_fp_build ( )
dest_dir = os . path . join ( base_build_dir , state . target )
og_path = os . path . join ( exe_path , dest_exe_name )
dest_path = os . path . join ( dest_dir , dest_exe_name )
os . makedirs ( dest_dir , exist_ok = True )
try :
log . info ( f " Moving ' { og_path } ' to ' { dest_dir } ' ... " )
shutil . move ( og_path , dest_dir )
cmd = [ dest_path ]
except Exception as e :
log . error ( f " Failed to move executable: { e } . Attempting to run from original location. " )
cmd = [ exe_path ]
os . chdir ( dest_dir )
log . info ( f " Hashlink final command: { ' ' . join ( cmd ) } " )
2025-01-22 16:18:30 +01:00
try :
state . proc_play = run_proc ( cmd , play_done )
except Exception :
traceback . print_exc ( )
log . warn ( ' Failed to start player, command and exception have been printed to console above ' )
if wrd . lnx_runtime == ' Browser ' :
webbrowser . open ( url )
elif state . is_publish :
sdk_path = lnx . utils . get_sdk_path ( )
target_name = lnx . utils . get_kha_target ( state . target )
files_path = os . path . join ( lnx . utils . get_fp_build ( ) , target_name )
if target_name in ( ' html5 ' , ' krom ' ) and wrd . lnx_minify_js :
# Minify JS
minifier_path = sdk_path + ' /lib/leenkx_tools/uglifyjs/bin/uglifyjs '
if target_name == ' html5 ' :
jsfile = files_path + ' /kha.js '
else :
jsfile = files_path + ' /krom.js '
args = [ lnx . utils . get_node_path ( ) , minifier_path , jsfile , ' -o ' , jsfile ]
proc = subprocess . Popen ( args )
proc . wait ( )
if target_name == ' krom ' :
# Copy Krom binaries
if state . target == ' krom-windows ' :
gapi = state . export_gapi
ext = ' ' if gapi == ' direct3d11 ' else ' _ ' + gapi
krom_location = sdk_path + ' /Krom/Krom ' + ext + ' .exe '
shutil . copy ( krom_location , files_path + ' /Krom.exe ' )
krom_exe = lnx . utils . safestr ( wrd . lnx_project_name ) + ' .exe '
os . rename ( files_path + ' /Krom.exe ' , files_path + ' / ' + krom_exe )
elif state . target == ' krom-linux ' :
krom_location = sdk_path + ' /Krom/Krom '
shutil . copy ( krom_location , files_path )
krom_exe = lnx . utils . safestr ( wrd . lnx_project_name )
os . rename ( files_path + ' /Krom ' , files_path + ' / ' + krom_exe )
krom_exe = ' ./ ' + krom_exe
else :
krom_location = sdk_path + ' /Krom/Krom.app '
shutil . copytree ( krom_location , files_path + ' /Krom.app ' )
game_files = os . listdir ( files_path )
for f in game_files :
f = files_path + ' / ' + f
if os . path . isfile ( f ) :
shutil . move ( f , files_path + ' /Krom.app/Contents/MacOS ' )
krom_exe = lnx . utils . safestr ( wrd . lnx_project_name ) + ' .app '
os . rename ( files_path + ' /Krom.app ' , files_path + ' / ' + krom_exe )
# Rename
ext = state . target . split ( ' - ' ) [ - 1 ] # krom-windows
new_files_path = files_path + ' - ' + ext
os . rename ( files_path , new_files_path )
files_path = new_files_path
if target_name == ' html5 ' :
project_path = files_path
print ( ' Exported HTML5 package to ' + project_path )
elif target_name . startswith ( ' ios ' ) or target_name . startswith ( ' osx ' ) : # TODO: to macos
project_path = files_path + ' -build '
print ( ' Exported XCode project to ' + project_path )
elif target_name . startswith ( ' windows ' ) :
project_path = files_path + ' -build '
vs_info = lnx . utils_vs . get_supported_version ( wrd . lnx_project_win_list_vs )
print ( f ' Exported { vs_info [ " name " ] } project to { project_path } ' )
elif target_name . startswith ( ' android ' ) :
project_name = lnx . utils . safesrc ( wrd . lnx_project_name + ' - ' + wrd . lnx_project_version )
project_path = os . path . join ( files_path + ' -build ' , project_name )
print ( ' Exported Android Studio project to ' + project_path )
elif target_name . startswith ( ' krom ' ) :
project_path = files_path
print ( ' Exported Krom package to ' + project_path )
else :
project_path = files_path + ' -build '
print ( ' Exported makefiles to ' + project_path )
if not bpy . app . background and lnx . utils . get_lnx_preferences ( ) . open_build_directory :
lnx . utils . open_folder ( project_path )
# Android build APK
if target_name . startswith ( ' android ' ) :
if ( lnx . utils . get_project_android_build_apk ( ) ) and ( len ( lnx . utils . get_android_sdk_root_path ( ) ) > 0 ) :
print ( " \n Building APK " )
# Check settings
path_sdk = lnx . utils . get_android_sdk_root_path ( )
if len ( path_sdk ) > 0 :
# Check Environment Variables - ANDROID_SDK_ROOT
if os . getenv ( ' ANDROID_SDK_ROOT ' ) is None :
# Set value from settings
os . environ [ ' ANDROID_SDK_ROOT ' ] = path_sdk
else :
project_path = ' '
# Build start
if len ( project_path ) > 0 :
os . chdir ( project_path ) # set work folder
if lnx . utils . get_os_is_windows ( ) :
state . proc_publish_build = run_proc ( os . path . join ( project_path , " gradlew.bat assembleDebug " ) , done_gradlew_build )
else :
cmd = shlex . split ( os . path . join ( project_path , " gradlew assembleDebug " ) )
state . proc_publish_build = run_proc ( cmd , done_gradlew_build )
else :
print ( ' \n Building APK Warning: ANDROID_SDK_ROOT is not specified in environment variables and " Android SDK Path " setting is not specified in preferences: \n - If you specify an environment variable ANDROID_SDK_ROOT, then you need to restart Blender; \n - If you specify the setting " Android SDK Path " in the preferences, then repeat operation " Publish " ' )
# HTML5 After Publish
if target_name . startswith ( ' html5 ' ) :
if len ( lnx . utils . get_html5_copy_path ( ) ) > 0 and ( wrd . lnx_project_html5_copy ) :
project_name = lnx . utils . safesrc ( wrd . lnx_project_name + ' - ' + wrd . lnx_project_version )
dst = os . path . join ( lnx . utils . get_html5_copy_path ( ) , project_name )
if os . path . exists ( dst ) :
shutil . rmtree ( dst )
try :
shutil . copytree ( project_path , dst )
print ( " Copied files to " + dst )
except OSError as exc :
if exc . errno == errno . ENOTDIR :
shutil . copy ( project_path , dst )
else : raise
if len ( lnx . utils . get_link_web_server ( ) ) and ( wrd . lnx_project_html5_start_browser ) :
link_html5_app = lnx . utils . get_link_web_server ( ) + ' / ' + project_name
print ( " Running a browser with a link " + link_html5_app )
webbrowser . open ( link_html5_app )
# Windows After Publish
if target_name . startswith ( ' windows ' ) and wrd . lnx_project_win_build != ' nothing ' and lnx . utils . get_os_is_windows ( ) :
project_name = lnx . utils . safesrc ( wrd . lnx_project_name + ' - ' + wrd . lnx_project_version )
# Open in Visual Studio
if wrd . lnx_project_win_build == ' open ' :
print ( ' \n Opening in Visual Studio: ' + lnx . utils_vs . get_sln_path ( ) )
_ = lnx . utils_vs . open_project_in_vs ( wrd . lnx_project_win_list_vs )
# Compile
elif wrd . lnx_project_win_build . startswith ( ' compile ' ) :
if wrd . lnx_project_win_build == ' compile ' :
print ( ' \n Compiling project ' + lnx . utils_vs . get_vcxproj_path ( ) )
elif wrd . lnx_project_win_build == ' compile_and_run ' :
print ( ' \n Compiling and running project ' + lnx . utils_vs . get_vcxproj_path ( ) )
success = lnx . utils_vs . enable_vsvars_env ( wrd . lnx_project_win_list_vs , done_vs_vars )
if not success :
state . redraw_ui = True
log . error ( ' Compile failed, check console ' )
def done_gradlew_build ( ) :
if state . proc_publish_build is None :
return
result = state . proc_publish_build . poll ( )
if result == 0 :
state . proc_publish_build = None
wrd = bpy . data . worlds [ ' Lnx ' ]
path_apk = os . path . join ( lnx . utils . get_fp_build ( ) , lnx . utils . get_kha_target ( state . target ) )
project_name = lnx . utils . safesrc ( wrd . lnx_project_name + ' - ' + wrd . lnx_project_version )
path_apk = os . path . join ( path_apk + ' -build ' , project_name , ' app ' , ' build ' , ' outputs ' , ' apk ' , ' debug ' )
print ( " \n Build APK to " + path_apk )
# Rename APK
apk_name = ' app-debug.apk '
file_name = os . path . join ( path_apk , apk_name )
if wrd . lnx_project_android_rename_apk :
apk_name = project_name + ' .apk '
os . rename ( file_name , os . path . join ( path_apk , apk_name ) )
file_name = os . path . join ( path_apk , apk_name )
print ( " \n Rename APK to " + apk_name )
# Copy APK
if wrd . lnx_project_android_copy_apk :
shutil . copyfile ( file_name , os . path . join ( lnx . utils . get_android_apk_copy_path ( ) , apk_name ) )
print ( " Copy APK to " + lnx . utils . get_android_apk_copy_path ( ) )
# Open directory with APK
if lnx . utils . get_android_open_build_apk_directory ( ) :
lnx . utils . open_folder ( path_apk )
# Open directory after copy APK
if lnx . utils . get_android_apk_copy_open_directory ( ) :
lnx . utils . open_folder ( lnx . utils . get_android_apk_copy_path ( ) )
# Running emulator
if wrd . lnx_project_android_run_avd :
run_android_emulators ( lnx . utils . get_android_emulator_name ( ) )
state . redraw_ui = True
else :
state . proc_publish_build = None
state . redraw_ui = True
os . environ [ ' ANDROID_SDK_ROOT ' ] = ' '
log . error ( ' Building the APK failed, check console ' )
def run_android_emulators ( avd_name ) :
if len ( avd_name . strip ( ) ) == 0 :
return
print ( ' \n Running Emulator " ' + avd_name + ' " ' )
path_file = lnx . utils . get_android_emulator_file ( )
if len ( path_file ) > 0 :
if lnx . utils . get_os_is_windows ( ) :
run_proc ( path_file + " -avd " + avd_name , None )
else :
cmd = shlex . split ( path_file + " -avd " + avd_name )
run_proc ( cmd , None )
else :
print ( ' Update List Emulators Warning: File " ' + path_file + ' " not found. Check that the variable ANDROID_SDK_ROOT is correct in environment variables or in " Android SDK Path " setting: \n - If you specify an environment variable ANDROID_SDK_ROOT, then you need to restart Blender; \n - If you specify the setting " Android SDK Path " , then repeat operation " Publish " ' )
def done_vs_vars ( ) :
if state . proc_publish_build is None :
return
result = state . proc_publish_build . poll ( )
if result == 0 :
state . proc_publish_build = None
wrd = bpy . data . worlds [ ' Lnx ' ]
success = lnx . utils_vs . compile_in_vs ( wrd . lnx_project_win_list_vs , done_vs_build )
if not success :
state . proc_publish_build = None
state . redraw_ui = True
log . error ( ' Compile failed, check console ' )
else :
state . proc_publish_build = None
state . redraw_ui = True
log . error ( ' Compile failed, check console ' )
def done_vs_build ( ) :
if state . proc_publish_build is None :
return
result = state . proc_publish_build . poll ( )
if result == 0 :
state . proc_publish_build = None
wrd = bpy . data . worlds [ ' Lnx ' ]
project_path = os . path . join ( lnx . utils . get_fp_build ( ) , lnx . utils . get_kha_target ( state . target ) ) + ' -build '
if wrd . lnx_project_win_build_arch == ' x64 ' :
path = os . path . join ( project_path , ' x64 ' , wrd . lnx_project_win_build_mode )
else :
path = os . path . join ( project_path , wrd . lnx_project_win_build_mode )
print ( ' \n Compilation completed in ' + path )
# Run
if wrd . lnx_project_win_build == ' compile_and_run ' :
# Copying the executable file
res_path = os . path . join ( lnx . utils . get_fp_build ( ) , lnx . utils . get_kha_target ( state . target ) )
file_name = lnx . utils . safesrc ( wrd . lnx_project_name + ' - ' + wrd . lnx_project_version ) + ' .exe '
print ( ' \n Copy the executable file from ' + path + ' to ' + res_path )
shutil . copyfile ( os . path . join ( path , file_name ) , os . path . join ( res_path , file_name ) )
path = res_path
# Run project
cmd = os . path . join ( ' " ' + res_path , file_name + ' " ' )
print ( ' Run the executable file to ' + cmd )
os . chdir ( res_path ) # set work folder
subprocess . Popen ( cmd , shell = True )
# Open Build Directory
if wrd . lnx_project_win_build_open :
lnx . utils . open_folder ( path )
state . redraw_ui = True
else :
state . proc_publish_build = None
state . redraw_ui = True
log . error ( ' Compile failed, check console ' )
def clean ( ) :
os . chdir ( lnx . utils . get_fp ( ) )
wrd = bpy . data . worlds [ ' Lnx ' ]
# Remove build and compiled data
try :
if os . path . isdir ( lnx . utils . build_dir ( ) ) :
shutil . rmtree ( lnx . utils . build_dir ( ) , onerror = remove_readonly )
if os . path . isdir ( lnx . utils . get_fp ( ) + ' /build ' ) : # Kode Studio build dir
shutil . rmtree ( lnx . utils . get_fp ( ) + ' /build ' , onerror = remove_readonly )
except :
print ( ' Leenkx Warning: Some files in the build folder are locked ' )
# Remove compiled nodes
pkg_dir = lnx . utils . safestr ( wrd . lnx_project_package ) . replace ( ' . ' , ' / ' )
nodes_path = ' Sources/ ' + pkg_dir + ' /node/ '
if os . path . isdir ( nodes_path ) :
shutil . rmtree ( nodes_path , onerror = remove_readonly )
# Remove khafile/Main.hx
if os . path . isfile ( ' khafile.js ' ) :
os . remove ( ' khafile.js ' )
if os . path . isfile ( ' Sources/Main.hx ' ) :
os . remove ( ' Sources/Main.hx ' )
# Remove Sources/ dir if empty
if os . path . exists ( ' Sources/ ' + pkg_dir ) and os . listdir ( ' Sources/ ' + pkg_dir ) == [ ] :
shutil . rmtree ( ' Sources/ ' + pkg_dir , onerror = remove_readonly )
if os . path . exists ( ' Sources ' ) and os . listdir ( ' Sources ' ) == [ ] :
shutil . rmtree ( ' Sources/ ' , onerror = remove_readonly )
# Remove Shape key Textures
if os . path . exists ( ' MorphTargets/ ' ) :
shutil . rmtree ( ' MorphTargets/ ' , onerror = remove_readonly )
# To recache signatures for batched materials
for mat in bpy . data . materials :
mat . signature = ' '
mat . lnx_cached = False
# Restart compilation server
if lnx . utils . get_compilation_server ( ) :
lnx . lib . server . kill_haxe ( )
log . info ( ' Project cleaned ' )