LNXSDK/leenkx.py

951 lines
38 KiB
Python
Raw Permalink Normal View History

2025-01-22 14:19:54 +00:00
# Leenkx Full Stack SDK
# https://leenkx.com/
bl_info = {
"name": "Leenkx",
"category": "Software Development",
"location": "Properties -> Render -> Leenkx Player",
"description": "Full Stack SDK",
"author": "Leenkx.com",
"version": (1, 0, 8),
"blender": (4, 2, 1),
"doc_url": "https://leenkx.com/",
"tracker_url": "https://leenkx.com/support"
}
from enum import IntEnum
import os
from pathlib import Path
import platform
import re
import shutil
import stat
import subprocess
import sys
import textwrap
import threading
import traceback
import typing
from typing import Callable, Optional
import webbrowser
import bpy
from bpy.app.handlers import persistent
from bpy.props import *
from bpy.types import Operator, AddonPreferences
class SDKSource(IntEnum):
PREFS = 0
LOCAL = 1
ENV_VAR = 2
# Keep the value of these globals after addon reload
if "is_running" not in locals():
is_running = False
last_sdk_path = ""
last_scripts_path = ""
sdk_source = SDKSource.PREFS
update_error_msg = ''
def get_os():
s = platform.system()
if s == 'Windows':
return 'win'
elif s == 'Darwin':
return 'mac'
else:
return 'linux'
def detect_sdk_path():
"""Auto-detect the SDK path after Leenkx installation."""
# Do not overwrite the SDK path (this method gets
# called after each registration, not after
# installation only)
preferences = bpy.context.preferences
addon_prefs = preferences.addons["leenkx"].preferences
if addon_prefs.sdk_path != "":
return
win = bpy.context.window_manager.windows[0]
area = win.screen.areas[0]
area_type = area.type
area.type = "INFO"
with bpy.context.temp_override(window=win, screen=win.screen, area=area):
bpy.ops.info.select_all(action='SELECT')
bpy.ops.info.report_copy()
area.type = area_type
clipboard = bpy.context.window_manager.clipboard
# If leenkx was installed multiple times in this session,
# use the latest log entry.
match = re.findall(r"^Modules Installed .* from '(.*leenkx.py)' into", clipboard, re.MULTILINE)
if match:
addon_prefs.sdk_path = os.path.dirname(match[-1])
def get_link_web_server(self):
return self.get('link_web_server', 'http://localhost/')
def set_link_web_server(self, value):
regex = re.compile(
r'^(?:http|ftp)s?://' # http:// or https://
r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' #domain...
r'localhost|' #localhost...
r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # ...or ip
r'(?::\d+)?' # optional port
r'(?:/?|[/?]\S+)$', re.IGNORECASE)
if re.match(regex, value) is not None:
self['link_web_server'] = value
class LeenkxAddonPreferences(AddonPreferences):
bl_idname = __name__
def sdk_path_update(self, context):
if self.skip_update:
return
self.skip_update = True
self.sdk_path = bpy.path.reduce_dirs([bpy.path.abspath(self.sdk_path)])[0] + '/'
restart_leenkx(context)
def ide_bin_update(self, context):
if self.skip_update:
return
self.skip_update = True
self.ide_bin = bpy.path.reduce_dirs([bpy.path.abspath(self.ide_bin)])[0]
def ffmpeg_path_update(self, context):
if self.skip_update or self.ffmpeg_path == '':
return
self.skip_update = True
self.ffmpeg_path = bpy.path.reduce_dirs([bpy.path.abspath(self.ffmpeg_path)])[0]
def renderdoc_path_update(self, context):
if self.skip_update or self.renderdoc_path == '':
return
self.skip_update = True
self.renderdoc_path = bpy.path.reduce_dirs([bpy.path.abspath(self.renderdoc_path)])[0]
def android_sdk_path_update(self, context):
if self.skip_update:
return
self.skip_update = True
self.android_sdk_root_path = bpy.path.reduce_dirs([bpy.path.abspath(self.android_sdk_root_path)])[0]
def android_apk_copy_update(self, context):
if self.skip_update:
return
self.skip_update = True
self.android_apk_copy_path = bpy.path.reduce_dirs([bpy.path.abspath(self.android_apk_copy_path)])[0]
def html5_copy_path_update(self, context):
if self.skip_update:
return
self.skip_update = True
self.html5_copy_path = bpy.path.reduce_dirs([bpy.path.abspath(self.html5_copy_path)])[0]
sdk_path: StringProperty(name="SDK Path", subtype="FILE_PATH", update=sdk_path_update, default="")
update_submodules: BoolProperty(
name="Update Submodules", default=True, description=(
"If enabled, update the submodules to their most current commit after downloading the SDK."
" Otherwise, the submodules are checked out at whatever commit the latest SDK references."
)
)
show_advanced: BoolProperty(name="Show Advanced", default=False)
tabs: EnumProperty(
items=[('general', 'General', 'General Settings'),
('build', 'Build Preferences', 'Settings related to building the game'),
('debugconsole', 'Debug Console', 'Settings related to the in-game debug console'),
('dev', 'Developer Settings', 'Settings for Leenkx developers')],
name='Tabs', default='general', description='Choose the settings page you want to see')
ide_bin: StringProperty(name="Code Editor Executable", subtype="FILE_PATH", update=ide_bin_update, default="", description="Path to your editor's executable file")
code_editor: EnumProperty(
items = [('default', 'System Default', 'System Default'),
('kodestudio', 'VS Code | Kode Studio', 'Visual Studio Code or Kode Studio'),
('sublime', 'Sublime Text', 'Sublime Text'),
('custom', "Custom", "Use a Custom Code Editor")],
name="Code Editor", default='default', description='Use this editor for editing scripts')
ui_scale: FloatProperty(name='UI Scale', description='Adjust UI scale for Leenkx tools', default=1.0, min=1.0, max=4.0)
khamake_threads: IntProperty(name='Khamake Processes', description='Allow Khamake to spawn multiple processes for faster builds', default=4, min=1)
khamake_threads_use_auto: BoolProperty(name='Auto', description='Let Khamake choose the number of processes automatically', default=False)
compilation_server: BoolProperty(name='Compilation Server', description='Allow Haxe to create a local compilation server for faster builds', default=True)
renderdoc_path: StringProperty(name="RenderDoc Path", description="Binary path", subtype="FILE_PATH", update=renderdoc_path_update, default="")
ffmpeg_path: StringProperty(name="FFMPEG Path", description="Binary path", subtype="FILE_PATH", update=ffmpeg_path_update, default="")
save_on_build: BoolProperty(name="Save on Build", description="Save .blend", default=False)
open_build_directory: BoolProperty(name="Open Build Directory After Publishing", description="Open the build directory after successfully publishing the project", default=False)
cmft_use_opencl: BoolProperty(
name="CMFT: Use OpenCL", default=True,
description=(
"Whether to use OpenCL processing to generate radiance maps with CMFT."
" If you experience extremely long build times caused by CMFT, try disabling this option."
2025-01-23 15:59:04 +00:00
" For more information see https://dev.leenkx.com/LeenkxTeam/LNXSDK/issues/2760"
2025-01-22 14:19:54 +00:00
))
legacy_shaders: BoolProperty(name="Legacy Shaders", description="Attempt to compile shaders runnable on older hardware, use this for WebGL1 or GLES2 support in mobile render path", default=False)
relative_paths: BoolProperty(name="Generate Relative Paths", description="Write relative paths in khafile", default=False)
viewport_controls: EnumProperty(
items=[('qwerty', 'qwerty', 'qwerty'),
('azerty', 'azerty', 'azerty')],
name="Viewport Controls", default='qwerty', description='Viewport camera mode controls')
skip_update: BoolProperty(name="", default=False)
# Debug Console
debug_console_auto: BoolProperty(name="Enable Debug Console for new project", description="Enable Debug Console for new project", default=False)
# Shortcuts
items_enum_keyboard = [ ('192', '~', 'TILDE'),
('219', '[', 'OPEN BRACKET'),
('221', ']', 'CLOSE BRACKET'),
('192', '`', 'BACK QUOTE'),
('57', '(', 'OPEN BRACKET'),
('48', ')', 'CLOSE BRACKET'),
('56', '*', 'MULTIPLY'),
('190', '.', 'PERIOD'),
('188', ',', 'COMMA', ),
('191', '/', 'SLASH'),
('65', 'A', 'A'),
('66', 'B', 'B'),
('67', 'C', 'C'),
('68', 'D', 'D'),
('69', 'E', 'E'),
('70', 'F', 'F'),
('71', 'G', 'G'),
('72', 'H', 'H'),
('73', 'I', 'I'),
('74', 'J', 'J'),
('75', 'K', 'K'),
('76', 'L', 'L'),
('77', 'M', 'M'),
('78', 'N', 'N'),
('79', 'O', 'O'),
('80', 'P', 'P'),
('81', 'Q', 'Q'),
('82', 'R', 'R'),
('83', 'S', 'S'),
('84', 'T', 'T'),
('85', 'U', 'U'),
('86', 'V', 'V'),
('87', 'W', 'W'),
('88', 'X', 'X'),
('89', 'Y', 'Y'),
('90', 'Z', 'Z'),
('48', '0', '0'),
('49', '1', '1'),
('50', '2', '2'),
('51', '3', '3'),
('52', '4', '4'),
('53', '5', '5'),
('54', '6', '6'),
('55', '7', '7'),
('56', '8', '8'),
('57', '9', '9'),
('32', 'SPACE', 'SPACE'),
('8', 'BACKSPACE', 'BACKSPACE'),
('9', 'TAB', 'TAB'),
('13', 'ENTER', 'ENTER'),
('16', 'SHIFT', 'SHIFT'),
('17', 'CONTROL', 'CONTROL'),
('18', 'ALT', 'ALT'),
('27', 'ESCAPE', 'ESCAPE'),
('46', 'DELETE', 'DELETE'),
('33', 'PAGE UP', 'PAGE UP'),
('34', 'PAGE DOWN', 'PAGE DOWN'),
('38', 'UP', 'UP'),
('39', 'RIGHT', 'RIGHT'),
('37', 'LEFT', 'LEFT'),
('40', 'DOWN', 'DOWN'),
('96', 'NUMPAD 0', 'NUMPAD 0'),
('97', 'NUMPAD 1', 'NUMPAD 1'),
('98', 'NUMPAD 2', 'NUMPAD 2'),
('99', 'NUMPAD 3', 'NUMPAD 3'),
('100', 'NUMPAD 4', 'NUMPAD 4'),
('101', 'NUMPAD 5', 'NUMPAD 5'),
('102', 'NUMPAD 6', 'NUMPAD 6'),
('103', 'NUMPAD 7', 'NUMPAD 7'),
('104', 'NUMPAD 8', 'NUMPAD 8'),
('106', 'NUMPAD *', 'NUMPAD *'),
('110', 'NUMPAD /', 'NUMPAD /'),
('107', 'NUMPAD +', 'NUMPAD +'),
('108', 'NUMPAD -', 'NUMPAD -'),
('109', 'NUMPAD .', 'NUMPAD .')]
debug_console_visible_sc: EnumProperty(items = items_enum_keyboard,
name="Visible / Invisible Shortcut", description="Shortcut to display the console", default='192')
debug_console_scale_in_sc: EnumProperty(items = items_enum_keyboard,
name="Scale In Shortcut", description="Shortcut to scale in on the console", default='219')
debug_console_scale_out_sc: EnumProperty(items = items_enum_keyboard,
name="Scale Out Shortcut", description="Shortcut to scale out on the console", default='221')
# Android Settings
android_sdk_root_path: StringProperty(name="Android SDK Path", description="Path to the Android SDK installation directory", default="", subtype="FILE_PATH", update=android_sdk_path_update)
android_open_build_apk_directory: BoolProperty(name="Open Build APK Directory", description="Open the build APK directory after successfully assemble", default=False)
android_apk_copy_path: StringProperty(name="Copy APK To Folder", description="Copy the APK file to the folder after build", default="", subtype="FILE_PATH", update=android_apk_copy_update)
android_apk_copy_open_directory: BoolProperty(name="Open Directory After Copy", description="Open the directory after copy the APK file", default=False)
# HTML5 Settings
html5_copy_path: StringProperty(name="HTML5 Copy Path", description="Path to copy project after successfully publish", default="", subtype="FILE_PATH", update=html5_copy_path_update)
link_web_server: StringProperty(name="Url To Web Server", description="Url to the web server that runs the local server", default="http://localhost/", set=set_link_web_server, get=get_link_web_server)
html5_server_port: IntProperty(name="Web Server Port", description="The port number of the local web server", default=8040, min=1024, max=65535)
html5_server_log: BoolProperty(name="Enable Http Log", description="Enable logging of http requests to local web server", default=True)
# Developer options
profile_exporter: BoolProperty(
name="Exporter Profiling", default=False,
description="Run profiling when exporting the scene. A file named 'profile_exporter.prof' with the results will"
" be saved into the SDK directory and can be opened with tools such as SnakeViz")
khamake_debug: BoolProperty(
name="Set Khamake Flag: --debug", default=False,
description="Set the --debug flag when running Khamake. Useful for debugging HLSL shaders with RenderDoc")
haxe_times: BoolProperty(
name="Set Haxe Flag: --times", default=False,
description="Set the --times flag when running Haxe.")
use_leenkx_py_symlink: BoolProperty(
name="Symlink leenkx.py", default=False,
description=("Automatically symlink the registered leenkx.py with the original leenkx.py from the SDK for faster"
" development. Warning: this will invalidate the installation if the SDK is removed"),
update=lambda self, context: update_leenkx_py(get_sdk_path(context)),
)
def draw(self, context):
self.skip_update = False
layout = self.layout
layout.label(text="Welcome to Leenkx!")
# Compare version Blender and Leenkx (major, minor)
2025-01-23 17:05:07 +00:00
if bpy.app.version[0] != 4 or bpy.app.version[1] != 2:
2025-01-22 14:19:54 +00:00
box = layout.box().column()
2025-01-23 17:05:07 +00:00
box.label(text="Warning: For Leenkx to work correctly, use a Blender LTS version such as 4.2 | 3.6 | 3.3")
2025-01-22 14:19:54 +00:00
layout.prop(self, "sdk_path")
sdk_path = get_sdk_path(context)
if os.path.exists(sdk_path + '/leenkx') or os.path.exists(sdk_path + '/leenkx_backup'):
sdk_exists = True
else:
sdk_exists = False
if not sdk_exists:
layout.label(text="The directory will be created.")
elif sdk_source != SDKSource.PREFS:
row = layout.row()
row.alert = True
if sdk_source == SDKSource.LOCAL:
row.label(text=f'Using local SDK from {sdk_path}')
elif sdk_source == SDKSource.ENV_VAR:
row.label(text=f'Using SDK from "LNXSDK" environment variable: {sdk_path}')
box = layout.box().column()
row = box.row(align=True)
row.label(text="Leenkx SDK Manager")
col = row.column()
col.alignment = "RIGHT"
col.operator("lnx_addon.help", icon="URL")
box.label(text="Note: Development version may run unstable!")
box.separator()
box.prop(self, "update_submodules")
row = box.row(align=True)
row.alignment = 'EXPAND'
row.operator("lnx_addon.print_version_info", icon="INFO")
if sdk_exists:
row.operator("lnx_addon.update", icon="FILE_REFRESH")
else:
row.operator("lnx_addon.install", icon="IMPORT")
row.operator("lnx_addon.restore", icon="LOOP_BACK")
if update_error_msg != '':
col = box.column(align=True)
col.scale_y = 0.8
col.alignment = 'EXPAND'
col.alert = True
# Roughly estimate how much text fits in the current region's
# width (approximation of box width)
textwrap_width = int(bpy.context.region.width / 6)
col.label(text='An error occured:')
lines = textwrap.wrap(update_error_msg, width=textwrap_width, break_long_words=True, initial_indent=' ', subsequent_indent=' ')
for line in lines:
col.label(text=line)
box.label(text="Check console for download progress. Please restart Blender after successful SDK update.")
col = layout.column(align=(not self.show_advanced))
col.prop(self, "show_advanced")
if self.show_advanced:
box_main = col.box()
# Use a row to expand the prop horizontally
row = box_main.row()
row.scale_y = 1.2
row.ui_units_y = 1.4
row.prop(self, "tabs", expand=True)
box = box_main.column()
if self.tabs == "general":
box.prop(self, "code_editor")
if self.code_editor != "default":
box.prop(self, "ide_bin")
box.prop(self, "renderdoc_path")
box.prop(self, "ffmpeg_path")
box.prop(self, "viewport_controls")
box.prop(self, "ui_scale")
box.prop(self, "legacy_shaders")
box.prop(self, "relative_paths")
elif self.tabs == "build":
box.label(text="Build Preferences")
row = box.split(factor=0.8, align=True)
_col = row.column(align=True)
_col.enabled = not self.khamake_threads_use_auto
_col.prop(self, "khamake_threads")
row.prop(self, "khamake_threads_use_auto", toggle=True)
box.prop(self, "compilation_server")
box.prop(self, "open_build_directory")
box.prop(self, "save_on_build")
box.separator()
box.prop(self, "cmft_use_opencl")
box = box_main.column()
box.label(text="Android Settings")
box.prop(self, "android_sdk_root_path")
box.prop(self, "android_open_build_apk_directory")
box.prop(self, "android_apk_copy_path")
box.prop(self, "android_apk_copy_open_directory")
box = box_main.column()
box.label(text="HTML5 Settings")
box.prop(self, "html5_copy_path")
box.prop(self, "link_web_server")
box.prop(self, "html5_server_port")
box.prop(self, "html5_server_log")
elif self.tabs == "debugconsole":
box.label(text="Debug Console")
box.prop(self, "debug_console_auto")
box.label(text="Note: The following settings will be applied if Debug Console is enabled in the project settings")
box.prop(self, "debug_console_visible_sc")
box.prop(self, "debug_console_scale_in_sc")
box.prop(self, "debug_console_scale_out_sc")
elif self.tabs == "dev":
col = box.column(align=True)
col.label(icon="ERROR", text="Warning: The following settings are meant for Leenkx developers and might slow")
col.label(icon="BLANK1", text="down Leenkx or make it unstable. Only change them if you know what you are doing.")
box.separator()
col = box.column(align=True)
col.prop(self, "profile_exporter")
col.prop(self, "khamake_debug")
col.prop(self, "haxe_times")
col = box.column(align=True)
col.prop(self, "use_leenkx_py_symlink")
@staticmethod
def get_prefs() -> 'LeenkxAddonPreferences':
preferences = bpy.context.preferences
return typing.cast(LeenkxAddonPreferences, preferences.addons["leenkx"].preferences)
def get_fp():
if bpy.data.filepath == '':
return ''
s = bpy.data.filepath.split(os.path.sep)
s.pop()
return os.path.sep.join(s)
def same_path(path1: str, path2: str) -> bool:
"""Compare whether two paths point to the same location."""
if os.path.exists(path1) and os.path.exists(path2):
return os.path.samefile(path1, path2)
p1 = os.path.realpath(os.path.normpath(os.path.normcase(path1)))
p2 = os.path.realpath(os.path.normpath(os.path.normcase(path2)))
return p1 == p2
def get_sdk_path(context: bpy.context) -> str:
"""Returns the absolute path of the currently set Leenkx SDK.
The path is read from the following sources in that priority (the
topmost source is used if valid):
1. Environment variable 'LNXSDK' (must be an absolute path).
Useful to temporarily override the SDK path, e.g. when
running from the command line.
2. Local SDK in /lnxsdk relative to the current file.
3. The SDK path specified in the add-on preferences.
"""
global sdk_source
sdk_envvar = os.environ.get('LNXSDK')
if sdk_envvar is not None and os.path.isabs(sdk_envvar) and os.path.isdir(sdk_envvar) and os.path.exists(sdk_envvar):
sdk_source = SDKSource.ENV_VAR
return sdk_envvar
fp = get_fp()
if fp != '': # blend file is not saved
local_sdk = os.path.join(fp, 'lnxsdk')
if os.path.exists(local_sdk):
sdk_source = SDKSource.LOCAL
return local_sdk
sdk_source = SDKSource.PREFS
preferences = context.preferences
addon_prefs = preferences.addons["leenkx"].preferences
return addon_prefs.sdk_path
def apply_unix_permissions(sdk):
"""Apply permissions to executable files in Linux and macOS
The .zip format does not preserve file permissions and will
cause every subprocess of Leenkx3D to not work at all. This
workaround fixes the issue so Leenkx releases will work.
"""
if get_os() == 'linux':
paths=[
os.path.join(sdk, "lib/leenkx_tools/cmft/cmft-linux64"),
os.path.join(sdk, "Krom/Krom"),
# NodeJS
os.path.join(sdk, "nodejs/node-linux32"),
os.path.join(sdk, "nodejs/node-linux64"),
os.path.join(sdk, "nodejs/node-linuxarm"),
# Kha tools x64
os.path.join(sdk, "Kha/Tools/linux_x64/haxe"),
os.path.join(sdk, "Kha/Tools/linux_x64/lame"),
os.path.join(sdk, "Kha/Tools/linux_x64/oggenc"),
# Kha tools arm64
os.path.join(sdk, "Kha/Tools/linux_arm64/haxe"),
os.path.join(sdk, "Kha/Tools/linux_arm64/lame"),
os.path.join(sdk, "Kha/Tools/linux_arm64/oggenc"),
# Kinc tools x64
os.path.join(sdk, "Kha/Kinc/Tools/linux_x64/kmake"),
os.path.join(sdk, "Kha/Kinc/Tools/linux_x64/kraffiti"),
os.path.join(sdk, "Kha/Kinc/Tools/linux_x64/krafix"),
# Kinc tools arm
os.path.join(sdk, "Kha/Kinc/Tools/linux_arm/kmake"),
os.path.join(sdk, "Kha/Kinc/Tools/linux_arm/kraffiti"),
os.path.join(sdk, "Kha/Kinc/Tools/linux_arm/krafix"),
# Kinc tools arm64
os.path.join(sdk, "Kha/Kinc/Tools/linux_arm64/kmake"),
os.path.join(sdk, "Kha/Kinc/Tools/linux_arm64/kraffiti"),
os.path.join(sdk, "Kha/Kinc/Tools/linux_arm64/krafix"),
]
for path in paths:
os.chmod(path, 0o777)
if get_os() == 'mac':
paths=[
os.path.join(sdk, "lib/leenkx_tools/cmft/cmft-osx"),
os.path.join(sdk, "nodejs/node-osx"),
os.path.join(sdk, "Krom/Krom.app/Contents/MacOS/Krom"),
# Kha tools
os.path.join(sdk, "Kha/Tools/macos/haxe"),
os.path.join(sdk, "Kha/Tools/macos/lame"),
os.path.join(sdk, "Kha/Tools/macos/oggenc"),
# Kinc tools
os.path.join(sdk, "Kha/Kinc/Tools/macos/kmake"),
os.path.join(sdk, "Kha/Kinc/Tools/macos/kraffiti"),
os.path.join(sdk, "Kha/Kinc/Tools/macos/krafix"),
]
for path in paths:
os.chmod(path, 0o777)
def remove_readonly(func, path, excinfo):
os.chmod(path, stat.S_IWRITE)
func(path)
def run_proc(cmd: list[str], done: Optional[Callable[[bool], None]] = None):
def fn(p, done):
p.wait()
if done is not None:
done(False)
p = None
try:
p = subprocess.Popen(cmd)
except OSError as err:
if done is not None:
done(True)
print("Running command:", *cmd, "\n")
if err.errno == 12:
print("Make sure there is enough space for the SDK (at least 500mb)")
elif err.errno == 13:
print("Permission denied, try modifying the permission of the sdk folder")
else:
print("error: " + str(err))
except Exception as err:
if done is not None:
done(True)
print("Running command:", *cmd, "\n")
print("error:", str(err), "\n")
else:
threading.Thread(target=fn, args=(p, done)).start()
return p
def set_update_error(msg: str, err: Exception):
traceback.print_exception(type(err), err, err.__traceback__)
global update_error_msg
update_error_msg = msg + ' The full error message has been printed to the console.'
def try_os_call(func: Callable, *args, **kwargs) -> bool:
try:
func(*args, **kwargs)
except OSError as err:
if hasattr(err, 'winerror'):
if err.winerror == 32: # file is used by another process (ERROR_SHARING_VIOLATION)
set_update_error((
f'The file/path {err.filename} is used by at least one other process (ERROR_SHARING_VIOLATION).'
' Please close all processes that reference it and try again.'
), err)
return False
set_update_error('There was an unknown error while updating/restoring the Leenkx SDK.', err)
return False
except Exception as err:
set_update_error('There was an unknown error while updating/restoring the Leenkx SDK.', err)
return False
return True
def git_clone(done: Callable[[bool], None], rootdir: str, gitn: str, subdir: str, recursive=False):
rootdir = os.path.normpath(rootdir)
path = rootdir + '/' + subdir if subdir != '' else rootdir
if os.path.exists(path) and not os.path.exists(path + '_backup'):
if not try_os_call(os.rename, path, path + '_backup'):
return
if os.path.exists(path):
shutil.rmtree(path, onerror=remove_readonly)
if recursive:
2025-01-23 15:59:04 +00:00
run_proc(['git', 'clone', '--recursive', 'https://dev.leenkx.com/' + gitn, path, '--depth', '1', '--shallow-submodules', '--jobs', '4'], done)
2025-01-22 14:19:54 +00:00
else:
2025-01-23 15:59:04 +00:00
run_proc(['git', 'clone', 'https://dev.leenkx.com/' + gitn, path, '--depth', '1'], done)
2025-01-22 14:19:54 +00:00
def git_test(self: bpy.types.Operator, required_version=None):
try:
p = subprocess.Popen(['git', '--version'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
output, _ = p.communicate()
except (OSError, Exception) as exception:
print(str(exception))
else:
matched = re.match("git version ([0-9]+).([0-9]+).([0-9]+)", output.decode('utf-8'))
if matched:
if required_version is not None:
matched_version = (int(matched.group(1)), int(matched.group(2)), int(matched.group(3)))
if matched_version < required_version:
msg = f"Installed git version {matched_version} is too old, please update git to version {required_version} or above"
print(msg)
self.report({"ERROR"}, msg)
return False
print('Git test succeeded.')
return True
msg = "Git test failed. Make sure git is installed (https://git-scm.com/downloads) or is working correctly."
print(msg)
self.report({"ERROR"}, msg)
return False
def restore_repo(rootdir: str, subdir: str):
rootdir = os.path.normpath(rootdir)
path = rootdir + '/' + subdir if subdir != '' else rootdir
if os.path.exists(path + '_backup'):
if os.path.exists(path):
if not try_os_call(shutil.rmtree, path, onerror=remove_readonly):
return
if not try_os_call(os.rename, path + '_backup', path):
# TODO: if rmtree() succeeds but rename fails the SDK needs
# manual cleanup by the user, add message to UI
return
class LnxAddonPrintVersionInfoButton(bpy.types.Operator):
bl_idname = "lnx_addon.print_version_info"
bl_label = "Print Version Info"
bl_description = "Print detailed information about the used SDK version to the console. This requires Git"
def execute(self, context):
sdk_path = get_sdk_path(context)
if sdk_path == "":
self.report({"ERROR"}, "Configure Leenkx SDK path first")
return {"CANCELLED"}
if not git_test(self):
return {"CANCELLED"}
if not os.path.exists(f'{sdk_path}/.git'):
msg=f"{sdk_path}/.git not found"
self.report({"ERROR"}, msg)
return {"CANCELLED"}
def print_version_info():
print("==============================")
print("| SDK: Current commit |")
print("==============================")
subprocess.check_call(["git", "branch", "-v"], cwd=sdk_path)
print("==============================")
print("| Submodules: Current commit |")
print("==============================")
subprocess.check_call(["git", "submodule", "status", "--recursive"], cwd=sdk_path)
print("==============================")
print("| SDK: Modified files |")
print("==============================")
subprocess.check_call(["git", "status", "--short"], cwd=sdk_path)
print("==============================")
print("| Submodules: Modified files |")
print("==============================")
subprocess.check_call(["git", "submodule", "foreach", "--recursive", "git status --short"], cwd=sdk_path)
print("Done.")
# Don't block UI
threading.Thread(target=print_version_info).start()
return {"FINISHED"}
class LnxAddonInstallButton(bpy.types.Operator):
"""Download and set up Leenkx SDK"""
bl_idname = "lnx_addon.install"
bl_label = "Download and set up SDK"
bl_description = "Download and set up the latest development version"
def execute(self, context):
download_sdk(self, context)
return {"FINISHED"}
class LnxAddonUpdateButton(bpy.types.Operator):
"""Update Leenkx SDK"""
bl_idname = "lnx_addon.update"
bl_label = "Update SDK"
bl_description = "Update to the latest development version"
def execute(self, context):
download_sdk(self, context)
return {"FINISHED"}
def download_sdk(self: bpy.types.Operator, context):
global update_error_msg
update_error_msg = ''
sdk_path = get_sdk_path(context)
if sdk_path == "":
self.report({"ERROR"}, "Configure Leenkx SDK path first")
return {"CANCELLED"}
self.report({'INFO'}, 'Downloading Leenkx SDK, check console for details.')
print('Leenkx (current add-on version' + str(bl_info['version']) + '): Cloning SDK repository recursively')
if not os.path.exists(sdk_path):
os.makedirs(sdk_path)
if not git_test(self):
return {"CANCELLED"}
preferences = context.preferences
addon_prefs = preferences.addons["leenkx"].preferences
def done(failed: bool):
if failed:
self.report({"ERROR"}, "Failed updating the SDK submodules, check console for details.")
else:
update_leenkx_py(sdk_path)
print('Leenkx SDK download completed, please restart Blender..')
def done_clone(failed: bool):
if failed:
self.report({"ERROR"}, "Failed downloading Leenkx SDK, check console for details.")
return
if addon_prefs.update_submodules:
if not git_test(self, (2, 34, 0)):
# For some unknown (and seemingly not fixable) reason, git submodule update --remote
# fails in earlier versions of Git with "fatal: Needed a single revision"
done(True)
else:
prev_cwd = os.getcwd()
os.chdir(sdk_path)
run_proc(['git', 'submodule', 'update', '--remote', '--depth', '1', '--jobs', '4'], done)
os.chdir(prev_cwd)
else:
done(False)
2025-01-23 15:59:04 +00:00
git_clone(done_clone, sdk_path, 'LeenkxTeam/LNXSDK', '', recursive=True)
2025-01-22 14:19:54 +00:00
class LnxAddonRestoreButton(bpy.types.Operator):
"""Update Leenkx SDK"""
bl_idname = "lnx_addon.restore"
bl_label = "Restore SDK"
bl_description = "Restore stable version"
def execute(self, context):
sdk_path = get_sdk_path(context)
if sdk_path == "":
self.report({"ERROR"}, "Configure Leenkx SDK path first")
return {"CANCELLED"}
restore_repo(sdk_path, '')
self.report({'INFO'}, 'Restored stable version')
return {"FINISHED"}
class LnxAddonHelpButton(bpy.types.Operator):
"""Updater help"""
bl_idname = "lnx_addon.help"
bl_label = "Help"
2025-01-23 15:59:04 +00:00
bl_description = "Leenkx Updater to get the latest SDK from https://dev.leenkx.com/LeenkxTeam/LNXSDK"
2025-01-22 14:19:54 +00:00
def execute(self, context):
webbrowser.open('https://leenkx.com/support')
return {"FINISHED"}
def update_leenkx_py(sdk_path: str, force_relink=False):
"""Ensure that leenkx.py is up to date by copying it from the
current SDK path (if 'use_leenkx_py_symlink' is true, a symlink is
created instead). Note that because the current version of leenkx.py
is already loaded as a Python module, this change lags one add-on
reload behind.
"""
addon_prefs = LeenkxAddonPreferences.get_prefs()
lnx_module_file = Path(sys.modules['leenkx'].__file__)
if addon_prefs.use_leenkx_py_symlink:
if not lnx_module_file.is_symlink() or force_relink:
lnx_module_file.unlink(missing_ok=True)
try:
lnx_module_file.symlink_to(Path(sdk_path) / 'leenkx.py')
except OSError as err:
if hasattr(err, 'winerror'):
if err.winerror == 1314: # ERROR_PRIVILEGE_NOT_HELD
# Manually copy the file to "simulate" symlink
shutil.copy(Path(sdk_path) / 'leenkx.py', lnx_module_file)
else:
raise err
else:
raise err
else:
lnx_module_file.unlink(missing_ok=True)
shutil.copy(Path(sdk_path) / 'leenkx.py', lnx_module_file)
def start_leenkx(sdk_path: str):
global is_running
global last_scripts_path
global last_sdk_path
if sdk_path == "":
return
leenkx_path = os.path.join(sdk_path, "leenkx")
if not os.path.exists(leenkx_path):
print("Leenkx load error: 'leenkx' folder not found in SDK path."
" Please make sure the SDK path is correct or that the SDK"
" was downloaded correctly.")
return
apply_unix_permissions(sdk_path)
scripts_path = os.path.join(leenkx_path, "blender")
sys.path.append(scripts_path)
last_scripts_path = scripts_path
update_leenkx_py(sdk_path, force_relink=True)
import start
if last_sdk_path != "":
import importlib
start = importlib.reload(start)
use_local_sdk = (sdk_source == SDKSource.LOCAL)
start.register(local_sdk=use_local_sdk)
last_sdk_path = sdk_path
is_running = True
print(f'Running Leenkx SDK from {sdk_path}')
def stop_leenkx():
global is_running
if not is_running:
return
import start
start.unregister()
sys.path.remove(last_scripts_path)
is_running = False
def restart_leenkx(context):
old_sdk_source = sdk_source
sdk_path = get_sdk_path(context)
if sdk_path == "":
if not is_running:
print("Configure Leenkx SDK path first")
stop_leenkx()
return
# Only restart Leenkx when the SDK path changed or it isn't running,
# otherwise we can keep the currently running instance
if not same_path(last_sdk_path, sdk_path) or sdk_source != old_sdk_source or not is_running:
stop_leenkx()
assert not is_running
start_leenkx(sdk_path)
@persistent
def on_load_post(context):
restart_leenkx(bpy.context) # context is None, use bpy.context instead
def on_register_post():
detect_sdk_path()
restart_leenkx(bpy.context)
def register():
bpy.utils.register_class(LeenkxAddonPreferences)
bpy.utils.register_class(LnxAddonPrintVersionInfoButton)
bpy.utils.register_class(LnxAddonInstallButton)
bpy.utils.register_class(LnxAddonUpdateButton)
bpy.utils.register_class(LnxAddonRestoreButton)
bpy.utils.register_class(LnxAddonHelpButton)
bpy.app.handlers.load_post.append(on_load_post)
# Hack to avoid _RestrictContext
bpy.app.timers.register(on_register_post, first_interval=0.01)
def unregister():
stop_leenkx()
bpy.utils.unregister_class(LeenkxAddonPreferences)
bpy.utils.unregister_class(LnxAddonInstallButton)
bpy.utils.unregister_class(LnxAddonPrintVersionInfoButton)
bpy.utils.unregister_class(LnxAddonUpdateButton)
bpy.utils.unregister_class(LnxAddonRestoreButton)
bpy.utils.unregister_class(LnxAddonHelpButton)
bpy.app.handlers.load_post.remove(on_load_post)
if __name__ == "__main__":
register()