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')