2025-01-22 16:18:30 +01:00

983 lines
40 KiB
Python

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
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)
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)
def export_data(fp, sdk_path):
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)
write_data.write_compiledglsl(defs + cdefs, make_variants=has_config)
# 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
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()
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()
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()
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()
build_dir = lnx.utils.build_dir()
path = '{}/debug/html5/'.format(build_dir)
url = 'http://{}:{}/{}'.format(host, prefs.html5_server_port, path)
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:
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:
if browsername in (None, '', 'default'):
webbrowser.open(url)
return
cmd = [browsername, url]
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')
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')