Files
LNXSDK/leenkx/blender/lnx/make.py

1545 lines
61 KiB
Python
Raw Normal View History

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)
appended_scenes = []
def load_external_blends():
global appended_scenes
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)
for scn in data_to.scenes:
if scn is not None and scn not in appended_scenes:
# make name unique with file name
scn.name += "_" + filename.replace(".blend", "")
appended_scenes.append(scn)
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():
global appended_scenes
if not appended_scenes:
return
for scn in appended_scenes:
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:
if lib.users == 0:
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}")
appended_scenes = []
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):
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:
ext = '.lz4' if LeenkxExporter.compress_enabled else '.lnx'
asset_path = build_dir + '/compiled/Assets/' + lnx.utils.safestr(scene.name) + ext
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
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("\nBuilding 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('\nBuilding 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('\nOpening 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('\nCompiling project ' + lnx.utils_vs.get_vcxproj_path())
elif wrd.lnx_project_win_build == 'compile_and_run':
print('\nCompiling 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("\nBuild 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("\nRename 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('\nRunning 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('\nCompilation 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('\nCopy 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')