armory.py 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938
  1. # Armory 3D Engine
  2. # https://github.com/armory3d/armory
  3. bl_info = {
  4. "name": "Armory",
  5. "category": "Game Engine",
  6. "location": "Properties -> Render -> Armory Player",
  7. "description": "3D Game Engine for Blender",
  8. "author": "Armory3D.org",
  9. "version": (2025, 9, 0),
  10. "blender": (4, 5, 0),
  11. "doc_url": "https://github.com/armory3d/armory/wiki",
  12. "tracker_url": "https://github.com/armory3d/armory/issues"
  13. }
  14. from enum import IntEnum
  15. import os
  16. from pathlib import Path
  17. import platform
  18. import re
  19. import shutil
  20. import stat
  21. import subprocess
  22. import sys
  23. import textwrap
  24. import threading
  25. import traceback
  26. import typing
  27. from typing import Callable, Optional
  28. import webbrowser
  29. import bpy
  30. from bpy.app.handlers import persistent
  31. from bpy.props import *
  32. from bpy.types import Operator, AddonPreferences
  33. class SDKSource(IntEnum):
  34. PREFS = 0
  35. LOCAL = 1
  36. ENV_VAR = 2
  37. # Keep the value of these globals after addon reload
  38. if "is_running" not in locals():
  39. is_running = False
  40. last_sdk_path = ""
  41. last_scripts_path = ""
  42. sdk_source = SDKSource.PREFS
  43. update_error_msg = ''
  44. def get_os():
  45. s = platform.system()
  46. if s == 'Windows':
  47. return 'win'
  48. elif s == 'Darwin':
  49. return 'mac'
  50. else:
  51. return 'linux'
  52. def detect_sdk_path():
  53. """Auto-detect the SDK path after Armory installation."""
  54. # Do not overwrite the SDK path (this method gets
  55. # called after each registration, not after
  56. # installation only)
  57. preferences = bpy.context.preferences
  58. addon_prefs = preferences.addons["armory"].preferences
  59. if addon_prefs.sdk_path != "":
  60. return
  61. win = bpy.context.window_manager.windows[0]
  62. area = win.screen.areas[0]
  63. area_type = area.type
  64. area.type = "INFO"
  65. with bpy.context.temp_override(window=win, screen=win.screen, area=area):
  66. bpy.ops.info.select_all(action='SELECT')
  67. bpy.ops.info.report_copy()
  68. area.type = area_type
  69. clipboard = bpy.context.window_manager.clipboard
  70. # If armory was installed multiple times in this session,
  71. # use the latest log entry.
  72. match = re.findall(r"^Modules Installed .* from '(.*armory.py)' into", clipboard, re.MULTILINE)
  73. if match:
  74. addon_prefs.sdk_path = os.path.dirname(match[-1])
  75. def get_link_web_server(self):
  76. return self.get('link_web_server', 'http://localhost/')
  77. def set_link_web_server(self, value):
  78. regex = re.compile(
  79. r'^(?:http|ftp)s?://' # http:// or https://
  80. r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' #domain...
  81. r'localhost|' #localhost...
  82. r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # ...or ip
  83. r'(?::\d+)?' # optional port
  84. r'(?:/?|[/?]\S+)$', re.IGNORECASE)
  85. if re.match(regex, value) is not None:
  86. self['link_web_server'] = value
  87. class ArmoryAddonPreferences(AddonPreferences):
  88. bl_idname = __name__
  89. def sdk_path_update(self, context):
  90. if self.skip_update:
  91. return
  92. self.skip_update = True
  93. self.sdk_path = bpy.path.reduce_dirs([bpy.path.abspath(self.sdk_path)])[0] + '/'
  94. restart_armory(context)
  95. def ide_bin_update(self, context):
  96. if self.skip_update:
  97. return
  98. self.skip_update = True
  99. self.ide_bin = bpy.path.reduce_dirs([bpy.path.abspath(self.ide_bin)])[0]
  100. def ffmpeg_path_update(self, context):
  101. if self.skip_update or self.ffmpeg_path == '':
  102. return
  103. self.skip_update = True
  104. self.ffmpeg_path = bpy.path.reduce_dirs([bpy.path.abspath(self.ffmpeg_path)])[0]
  105. def renderdoc_path_update(self, context):
  106. if self.skip_update or self.renderdoc_path == '':
  107. return
  108. self.skip_update = True
  109. self.renderdoc_path = bpy.path.reduce_dirs([bpy.path.abspath(self.renderdoc_path)])[0]
  110. def android_sdk_path_update(self, context):
  111. if self.skip_update:
  112. return
  113. self.skip_update = True
  114. self.android_sdk_root_path = bpy.path.reduce_dirs([bpy.path.abspath(self.android_sdk_root_path)])[0]
  115. def android_apk_copy_update(self, context):
  116. if self.skip_update:
  117. return
  118. self.skip_update = True
  119. self.android_apk_copy_path = bpy.path.reduce_dirs([bpy.path.abspath(self.android_apk_copy_path)])[0]
  120. def html5_copy_path_update(self, context):
  121. if self.skip_update:
  122. return
  123. self.skip_update = True
  124. self.html5_copy_path = bpy.path.reduce_dirs([bpy.path.abspath(self.html5_copy_path)])[0]
  125. sdk_path: StringProperty(name="SDK Path", subtype="FILE_PATH", update=sdk_path_update, default="")
  126. update_submodules: BoolProperty(
  127. name="Update Submodules", default=True, description=(
  128. "If enabled, update the submodules to their most current commit after downloading the SDK."
  129. " Otherwise, the submodules are checked out at whatever commit the latest SDK references."
  130. )
  131. )
  132. show_advanced: BoolProperty(name="Show Advanced", default=False)
  133. tabs: EnumProperty(
  134. items=[('general', 'General', 'General Settings'),
  135. ('build', 'Build Preferences', 'Settings related to building the game'),
  136. ('debugconsole', 'Debug Console', 'Settings related to the in-game debug console'),
  137. ('dev', 'Developer Settings', 'Settings for Armory developers')],
  138. name='Tabs', default='general', description='Choose the settings page you want to see')
  139. ide_bin: StringProperty(name="Code Editor Executable", subtype="FILE_PATH", update=ide_bin_update, default="", description="Path to your editor's executable file")
  140. code_editor: EnumProperty(
  141. items = [('default', 'System Default', 'System Default'),
  142. ('kodestudio', 'VS Code | Kode Studio', 'Visual Studio Code or Kode Studio'),
  143. ('sublime', 'Sublime Text', 'Sublime Text'),
  144. ('custom', "Custom", "Use a Custom Code Editor")],
  145. name="Code Editor", default='default', description='Use this editor for editing scripts')
  146. ui_scale: FloatProperty(name='UI Scale', description='Adjust UI scale for Armory tools', default=1.0, min=1.0, max=4.0)
  147. khamake_threads: IntProperty(name='Khamake Processes', description='Allow Khamake to spawn multiple processes for faster builds', default=4, min=1)
  148. khamake_threads_use_auto: BoolProperty(name='Auto', description='Let Khamake choose the number of processes automatically', default=False)
  149. compilation_server: BoolProperty(name='Compilation Server', description='Allow Haxe to create a local compilation server for faster builds', default=True)
  150. renderdoc_path: StringProperty(name="RenderDoc Path", description="Binary path", subtype="FILE_PATH", update=renderdoc_path_update, default="")
  151. ffmpeg_path: StringProperty(name="FFMPEG Path", description="Binary path", subtype="FILE_PATH", update=ffmpeg_path_update, default="")
  152. save_on_build: BoolProperty(name="Save on Build", description="Save .blend", default=False)
  153. open_build_directory: BoolProperty(name="Open Build Directory After Publishing", description="Open the build directory after successfully publishing the project", default=False)
  154. cmft_use_opencl: BoolProperty(
  155. name="CMFT: Use OpenCL", default=True,
  156. description=(
  157. "Whether to use OpenCL processing to generate radiance maps with CMFT."
  158. " If you experience extremely long build times caused by CMFT, try disabling this option."
  159. " For more information see https://github.com/armory3d/armory/issues/2760"
  160. ))
  161. 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)
  162. relative_paths: BoolProperty(name="Generate Relative Paths", description="Write relative paths in khafile", default=False)
  163. viewport_controls: EnumProperty(
  164. items=[('qwerty', 'qwerty', 'qwerty'),
  165. ('azerty', 'azerty', 'azerty')],
  166. name="Viewport Controls", default='qwerty', description='Viewport camera mode controls')
  167. skip_update: BoolProperty(name="", default=False)
  168. # Debug Console
  169. debug_console_auto: BoolProperty(name="Enable Debug Console for new project", description="Enable Debug Console for new project", default=False)
  170. # Shortcuts
  171. items_enum_keyboard = [ ('192', '~', 'TILDE'),
  172. ('219', '[', 'OPEN BRACKET'),
  173. ('221', ']', 'CLOSE BRACKET'),
  174. ('192', '`', 'BACK QUOTE'),
  175. ('57', '(', 'OPEN BRACKET'),
  176. ('48', ')', 'CLOSE BRACKET'),
  177. ('56', '*', 'MULTIPLY'),
  178. ('190', '.', 'PERIOD'),
  179. ('188', ',', 'COMMA', ),
  180. ('191', '/', 'SLASH'),
  181. ('65', 'A', 'A'),
  182. ('66', 'B', 'B'),
  183. ('67', 'C', 'C'),
  184. ('68', 'D', 'D'),
  185. ('69', 'E', 'E'),
  186. ('70', 'F', 'F'),
  187. ('71', 'G', 'G'),
  188. ('72', 'H', 'H'),
  189. ('73', 'I', 'I'),
  190. ('74', 'J', 'J'),
  191. ('75', 'K', 'K'),
  192. ('76', 'L', 'L'),
  193. ('77', 'M', 'M'),
  194. ('78', 'N', 'N'),
  195. ('79', 'O', 'O'),
  196. ('80', 'P', 'P'),
  197. ('81', 'Q', 'Q'),
  198. ('82', 'R', 'R'),
  199. ('83', 'S', 'S'),
  200. ('84', 'T', 'T'),
  201. ('85', 'U', 'U'),
  202. ('86', 'V', 'V'),
  203. ('87', 'W', 'W'),
  204. ('88', 'X', 'X'),
  205. ('89', 'Y', 'Y'),
  206. ('90', 'Z', 'Z'),
  207. ('48', '0', '0'),
  208. ('49', '1', '1'),
  209. ('50', '2', '2'),
  210. ('51', '3', '3'),
  211. ('52', '4', '4'),
  212. ('53', '5', '5'),
  213. ('54', '6', '6'),
  214. ('55', '7', '7'),
  215. ('56', '8', '8'),
  216. ('57', '9', '9'),
  217. ('32', 'SPACE', 'SPACE'),
  218. ('8', 'BACKSPACE', 'BACKSPACE'),
  219. ('9', 'TAB', 'TAB'),
  220. ('13', 'ENTER', 'ENTER'),
  221. ('16', 'SHIFT', 'SHIFT'),
  222. ('17', 'CONTROL', 'CONTROL'),
  223. ('18', 'ALT', 'ALT'),
  224. ('27', 'ESCAPE', 'ESCAPE'),
  225. ('46', 'DELETE', 'DELETE'),
  226. ('33', 'PAGE UP', 'PAGE UP'),
  227. ('34', 'PAGE DOWN', 'PAGE DOWN'),
  228. ('38', 'UP', 'UP'),
  229. ('39', 'RIGHT', 'RIGHT'),
  230. ('37', 'LEFT', 'LEFT'),
  231. ('40', 'DOWN', 'DOWN'),
  232. ('96', 'NUMPAD 0', 'NUMPAD 0'),
  233. ('97', 'NUMPAD 1', 'NUMPAD 1'),
  234. ('98', 'NUMPAD 2', 'NUMPAD 2'),
  235. ('99', 'NUMPAD 3', 'NUMPAD 3'),
  236. ('100', 'NUMPAD 4', 'NUMPAD 4'),
  237. ('101', 'NUMPAD 5', 'NUMPAD 5'),
  238. ('102', 'NUMPAD 6', 'NUMPAD 6'),
  239. ('103', 'NUMPAD 7', 'NUMPAD 7'),
  240. ('104', 'NUMPAD 8', 'NUMPAD 8'),
  241. ('106', 'NUMPAD *', 'NUMPAD *'),
  242. ('110', 'NUMPAD /', 'NUMPAD /'),
  243. ('107', 'NUMPAD +', 'NUMPAD +'),
  244. ('108', 'NUMPAD -', 'NUMPAD -'),
  245. ('109', 'NUMPAD .', 'NUMPAD .')]
  246. debug_console_visible_sc: EnumProperty(items = items_enum_keyboard,
  247. name="Visible / Invisible Shortcut", description="Shortcut to display the console", default='192')
  248. debug_console_scale_in_sc: EnumProperty(items = items_enum_keyboard,
  249. name="Scale In Shortcut", description="Shortcut to scale in on the console", default='219')
  250. debug_console_scale_out_sc: EnumProperty(items = items_enum_keyboard,
  251. name="Scale Out Shortcut", description="Shortcut to scale out on the console", default='221')
  252. # Android Settings
  253. 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)
  254. android_open_build_apk_directory: BoolProperty(name="Open Build APK Directory", description="Open the build APK directory after successfully assemble", default=False)
  255. 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)
  256. android_apk_copy_open_directory: BoolProperty(name="Open Directory After Copy", description="Open the directory after copy the APK file", default=False)
  257. # HTML5 Settings
  258. 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)
  259. 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)
  260. html5_server_port: IntProperty(name="Web Server Port", description="The port number of the local web server", default=8040, min=1024, max=65535)
  261. html5_server_log: BoolProperty(name="Enable Http Log", description="Enable logging of http requests to local web server", default=True)
  262. # Developer options
  263. profile_exporter: BoolProperty(
  264. name="Exporter Profiling", default=False,
  265. description="Run profiling when exporting the scene. A file named 'profile_exporter.prof' with the results will"
  266. " be saved into the SDK directory and can be opened with tools such as SnakeViz")
  267. khamake_debug: BoolProperty(
  268. name="Set Khamake Flag: --debug", default=False,
  269. description="Set the --debug flag when running Khamake. Useful for debugging HLSL shaders with RenderDoc")
  270. haxe_times: BoolProperty(
  271. name="Set Haxe Flag: --times", default=False,
  272. description="Set the --times flag when running Haxe.")
  273. use_armory_py_symlink: BoolProperty(
  274. name="Symlink armory.py", default=False,
  275. description=("Automatically symlink the registered armory.py with the original armory.py from the SDK for faster"
  276. " development. Warning: this will invalidate the installation if the SDK is removed"),
  277. update=lambda self, context: update_armory_py(get_sdk_path(context)),
  278. )
  279. def draw(self, context):
  280. self.skip_update = False
  281. layout = self.layout
  282. layout.label(text="Welcome to Armory!")
  283. # Compare version Blender and Armory (major, minor)
  284. if bpy.app.version[:2] not in [(4, 5), (4, 2), (3, 6), (3, 3)]:
  285. box = layout.box().column()
  286. box.label(text="Warning: For Armory to work correctly use a Blender LTS version.")
  287. layout.prop(self, "sdk_path")
  288. sdk_path = get_sdk_path(context)
  289. if os.path.exists(sdk_path + '/armory') or os.path.exists(sdk_path + '/armory_backup'):
  290. sdk_exists = True
  291. else:
  292. sdk_exists = False
  293. if not sdk_exists:
  294. layout.label(text="The directory will be created.")
  295. elif sdk_source != SDKSource.PREFS:
  296. row = layout.row()
  297. row.alert = True
  298. if sdk_source == SDKSource.LOCAL:
  299. row.label(text=f'Using local SDK from {sdk_path}')
  300. elif sdk_source == SDKSource.ENV_VAR:
  301. row.label(text=f'Using SDK from "ARMSDK" environment variable: {sdk_path}')
  302. box = layout.box().column()
  303. row = box.row(align=True)
  304. row.label(text="Armory SDK Manager")
  305. col = row.column()
  306. col.alignment = "RIGHT"
  307. col.operator("arm_addon.help", icon="URL")
  308. box.label(text="Note: Development version may run unstable!")
  309. box.separator()
  310. box.prop(self, "update_submodules")
  311. row = box.row(align=True)
  312. row.alignment = 'EXPAND'
  313. row.operator("arm_addon.print_version_info", icon="INFO")
  314. if sdk_exists:
  315. row.operator("arm_addon.update", icon="FILE_REFRESH")
  316. else:
  317. row.operator("arm_addon.install", icon="IMPORT")
  318. row.operator("arm_addon.restore", icon="LOOP_BACK")
  319. if update_error_msg != '':
  320. col = box.column(align=True)
  321. col.scale_y = 0.8
  322. col.alignment = 'EXPAND'
  323. col.alert = True
  324. # Roughly estimate how much text fits in the current region's
  325. # width (approximation of box width)
  326. textwrap_width = int(bpy.context.region.width / 6)
  327. col.label(text='An error occured:')
  328. lines = textwrap.wrap(update_error_msg, width=textwrap_width, break_long_words=True, initial_indent=' ', subsequent_indent=' ')
  329. for line in lines:
  330. col.label(text=line)
  331. box.label(text="Check console for download progress. Please restart Blender after successful SDK update.")
  332. col = layout.column(align=(not self.show_advanced))
  333. col.prop(self, "show_advanced")
  334. if self.show_advanced:
  335. box_main = col.box()
  336. # Use a row to expand the prop horizontally
  337. row = box_main.row()
  338. row.scale_y = 1.2
  339. row.ui_units_y = 1.4
  340. row.prop(self, "tabs", expand=True)
  341. box = box_main.column()
  342. if self.tabs == "general":
  343. box.prop(self, "code_editor")
  344. if self.code_editor != "default":
  345. box.prop(self, "ide_bin")
  346. box.prop(self, "renderdoc_path")
  347. box.prop(self, "ffmpeg_path")
  348. box.prop(self, "viewport_controls")
  349. box.prop(self, "ui_scale")
  350. box.prop(self, "legacy_shaders")
  351. box.prop(self, "relative_paths")
  352. elif self.tabs == "build":
  353. box.label(text="Build Preferences")
  354. row = box.split(factor=0.8, align=True)
  355. _col = row.column(align=True)
  356. _col.enabled = not self.khamake_threads_use_auto
  357. _col.prop(self, "khamake_threads")
  358. row.prop(self, "khamake_threads_use_auto", toggle=True)
  359. box.prop(self, "compilation_server")
  360. box.prop(self, "open_build_directory")
  361. box.prop(self, "save_on_build")
  362. box.separator()
  363. box.prop(self, "cmft_use_opencl")
  364. box = box_main.column()
  365. box.label(text="Android Settings")
  366. box.prop(self, "android_sdk_root_path")
  367. box.prop(self, "android_open_build_apk_directory")
  368. box.prop(self, "android_apk_copy_path")
  369. box.prop(self, "android_apk_copy_open_directory")
  370. box = box_main.column()
  371. box.label(text="HTML5 Settings")
  372. box.prop(self, "html5_copy_path")
  373. box.prop(self, "link_web_server")
  374. box.prop(self, "html5_server_port")
  375. box.prop(self, "html5_server_log")
  376. elif self.tabs == "debugconsole":
  377. box.label(text="Debug Console")
  378. box.prop(self, "debug_console_auto")
  379. box.label(text="Note: The following settings will be applied if Debug Console is enabled in the project settings")
  380. box.prop(self, "debug_console_visible_sc")
  381. box.prop(self, "debug_console_scale_in_sc")
  382. box.prop(self, "debug_console_scale_out_sc")
  383. elif self.tabs == "dev":
  384. col = box.column(align=True)
  385. col.label(icon="ERROR", text="Warning: The following settings are meant for Armory developers and might slow")
  386. col.label(icon="BLANK1", text="down Armory or make it unstable. Only change them if you know what you are doing.")
  387. box.separator()
  388. col = box.column(align=True)
  389. col.prop(self, "profile_exporter")
  390. col.prop(self, "khamake_debug")
  391. col.prop(self, "haxe_times")
  392. col = box.column(align=True)
  393. col.prop(self, "use_armory_py_symlink")
  394. @staticmethod
  395. def get_prefs() -> 'ArmoryAddonPreferences':
  396. preferences = bpy.context.preferences
  397. return typing.cast(ArmoryAddonPreferences, preferences.addons["armory"].preferences)
  398. def get_fp():
  399. if bpy.data.filepath == '':
  400. return ''
  401. s = bpy.data.filepath.split(os.path.sep)
  402. s.pop()
  403. return os.path.sep.join(s)
  404. def same_path(path1: str, path2: str) -> bool:
  405. """Compare whether two paths point to the same location."""
  406. if os.path.exists(path1) and os.path.exists(path2):
  407. return os.path.samefile(path1, path2)
  408. p1 = os.path.realpath(os.path.normpath(os.path.normcase(path1)))
  409. p2 = os.path.realpath(os.path.normpath(os.path.normcase(path2)))
  410. return p1 == p2
  411. def get_sdk_path(context: bpy.context) -> str:
  412. """Returns the absolute path of the currently set Armory SDK.
  413. The path is read from the following sources in that priority (the
  414. topmost source is used if valid):
  415. 1. Environment variable 'ARMSDK' (must be an absolute path).
  416. Useful to temporarily override the SDK path, e.g. when
  417. running from the command line.
  418. 2. Local SDK in /armsdk relative to the current file.
  419. 3. The SDK path specified in the add-on preferences.
  420. """
  421. global sdk_source
  422. sdk_envvar = os.environ.get('ARMSDK')
  423. if sdk_envvar is not None and os.path.isabs(sdk_envvar) and os.path.isdir(sdk_envvar) and os.path.exists(sdk_envvar):
  424. sdk_source = SDKSource.ENV_VAR
  425. return sdk_envvar
  426. fp = get_fp()
  427. if fp != '': # blend file is not saved
  428. local_sdk = os.path.join(fp, 'armsdk')
  429. if os.path.exists(local_sdk):
  430. sdk_source = SDKSource.LOCAL
  431. return local_sdk
  432. sdk_source = SDKSource.PREFS
  433. preferences = context.preferences
  434. addon_prefs = preferences.addons["armory"].preferences
  435. return addon_prefs.sdk_path
  436. def apply_unix_permissions(sdk):
  437. """Apply permissions to executable files in Linux and macOS
  438. The .zip format does not preserve file permissions and will
  439. cause every subprocess of Armory3D to not work at all. This
  440. workaround fixes the issue so Armory releases will work.
  441. """
  442. if get_os() == 'linux':
  443. paths=[
  444. os.path.join(sdk, "lib/armory_tools/cmft/cmft-linux64"),
  445. os.path.join(sdk, "Krom/Krom"),
  446. # NodeJS
  447. os.path.join(sdk, "nodejs/node-linux64"),
  448. # Kha tools x64
  449. os.path.join(sdk, "Kha/Tools/linux_x64/haxe"),
  450. os.path.join(sdk, "Kha/Tools/linux_x64/lame"),
  451. os.path.join(sdk, "Kha/Tools/linux_x64/oggenc"),
  452. # Kinc tools x64
  453. os.path.join(sdk, "Kha/Kinc/Tools/linux_x64/kmake"),
  454. os.path.join(sdk, "Kha/Kinc/Tools/linux_x64/kraffiti"),
  455. os.path.join(sdk, "Kha/Kinc/Tools/linux_x64/krafix"),
  456. ]
  457. for path in paths:
  458. os.chmod(path, 0o777)
  459. if get_os() == 'mac':
  460. paths=[
  461. os.path.join(sdk, "lib/armory_tools/cmft/cmft-osx"),
  462. os.path.join(sdk, "nodejs/node-osx"),
  463. os.path.join(sdk, "Krom/Krom.app/Contents/MacOS/Krom"),
  464. # Kha tools
  465. os.path.join(sdk, "Kha/Tools/macos/haxe"),
  466. os.path.join(sdk, "Kha/Tools/macos/lame"),
  467. os.path.join(sdk, "Kha/Tools/macos/oggenc"),
  468. # Kinc tools
  469. os.path.join(sdk, "Kha/Kinc/Tools/macos/kmake"),
  470. os.path.join(sdk, "Kha/Kinc/Tools/macos/kraffiti"),
  471. os.path.join(sdk, "Kha/Kinc/Tools/macos/krafix"),
  472. ]
  473. for path in paths:
  474. os.chmod(path, 0o777)
  475. def remove_readonly(func, path, excinfo):
  476. os.chmod(path, stat.S_IWRITE)
  477. func(path)
  478. def run_proc(cmd: list[str], done: Optional[Callable[[bool], None]] = None):
  479. def fn(p, done):
  480. p.wait()
  481. if done is not None:
  482. done(False)
  483. p = None
  484. try:
  485. p = subprocess.Popen(cmd)
  486. except OSError as err:
  487. if done is not None:
  488. done(True)
  489. print("Running command:", *cmd, "\n")
  490. if err.errno == 12:
  491. print("Make sure there is enough space for the SDK (at least 500mb)")
  492. elif err.errno == 13:
  493. print("Permission denied, try modifying the permission of the sdk folder")
  494. else:
  495. print("error: " + str(err))
  496. except Exception as err:
  497. if done is not None:
  498. done(True)
  499. print("Running command:", *cmd, "\n")
  500. print("error:", str(err), "\n")
  501. else:
  502. threading.Thread(target=fn, args=(p, done)).start()
  503. return p
  504. def set_update_error(msg: str, err: Exception):
  505. traceback.print_exception(type(err), err, err.__traceback__)
  506. global update_error_msg
  507. update_error_msg = msg + ' The full error message has been printed to the console.'
  508. def try_os_call(func: Callable, *args, **kwargs) -> bool:
  509. try:
  510. func(*args, **kwargs)
  511. except OSError as err:
  512. if hasattr(err, 'winerror'):
  513. if err.winerror == 32: # file is used by another process (ERROR_SHARING_VIOLATION)
  514. set_update_error((
  515. f'The file/path {err.filename} is used by at least one other process (ERROR_SHARING_VIOLATION).'
  516. ' Please close all processes that reference it and try again.'
  517. ), err)
  518. return False
  519. set_update_error('There was an unknown error while updating/restoring the Armory SDK.', err)
  520. return False
  521. except Exception as err:
  522. set_update_error('There was an unknown error while updating/restoring the Armory SDK.', err)
  523. return False
  524. return True
  525. def git_clone(done: Callable[[bool], None], rootdir: str, gitn: str, subdir: str, recursive=False):
  526. rootdir = os.path.normpath(rootdir)
  527. path = rootdir + '/' + subdir if subdir != '' else rootdir
  528. if os.path.exists(path) and not os.path.exists(path + '_backup'):
  529. if not try_os_call(os.rename, path, path + '_backup'):
  530. return
  531. if os.path.exists(path):
  532. shutil.rmtree(path, onerror=remove_readonly)
  533. if recursive:
  534. run_proc(['git', 'clone', '--recursive', 'https://github.com/' + gitn, path, '--depth', '1', '--shallow-submodules', '--jobs', '4'], done)
  535. else:
  536. run_proc(['git', 'clone', 'https://github.com/' + gitn, path, '--depth', '1'], done)
  537. def git_test(self: bpy.types.Operator, required_version=None):
  538. print('Testing if git is working...')
  539. try:
  540. p = subprocess.Popen(['git', '--version'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
  541. output, _ = p.communicate()
  542. except (OSError, Exception) as exception:
  543. print(str(exception))
  544. else:
  545. matched = re.match("git version ([0-9]+).([0-9]+).([0-9]+)", output.decode('utf-8'))
  546. if matched:
  547. if required_version is not None:
  548. matched_version = (int(matched.group(1)), int(matched.group(2)), int(matched.group(3)))
  549. if matched_version < required_version:
  550. msg = f"Installed git version {matched_version} is too old, please update git to version {required_version} or above"
  551. print(msg)
  552. self.report({"ERROR"}, msg)
  553. return False
  554. print('Git test succeeded.')
  555. return True
  556. msg = "Git test failed. Make sure git is installed (https://git-scm.com/downloads) or is working correctly."
  557. print(msg)
  558. self.report({"ERROR"}, msg)
  559. return False
  560. def restore_repo(rootdir: str, subdir: str):
  561. rootdir = os.path.normpath(rootdir)
  562. path = rootdir + '/' + subdir if subdir != '' else rootdir
  563. if os.path.exists(path + '_backup'):
  564. if os.path.exists(path):
  565. if not try_os_call(shutil.rmtree, path, onerror=remove_readonly):
  566. return
  567. if not try_os_call(os.rename, path + '_backup', path):
  568. # TODO: if rmtree() succeeds but rename fails the SDK needs
  569. # manual cleanup by the user, add message to UI
  570. return
  571. class ArmAddonPrintVersionInfoButton(bpy.types.Operator):
  572. bl_idname = "arm_addon.print_version_info"
  573. bl_label = "Print Version Info"
  574. bl_description = "Print detailed information about the used SDK version to the console. This requires Git"
  575. def execute(self, context):
  576. sdk_path = get_sdk_path(context)
  577. if sdk_path == "":
  578. self.report({"ERROR"}, "Configure Armory SDK path first")
  579. return {"CANCELLED"}
  580. if not git_test(self):
  581. return {"CANCELLED"}
  582. if not os.path.exists(f'{sdk_path}/.git'):
  583. msg=f"{sdk_path}/.git not found"
  584. self.report({"ERROR"}, msg)
  585. return {"CANCELLED"}
  586. def print_version_info():
  587. print("==============================")
  588. print("| SDK: Current commit |")
  589. print("==============================")
  590. subprocess.check_call(["git", "branch", "-v"], cwd=sdk_path)
  591. print("==============================")
  592. print("| Submodules: Current commit |")
  593. print("==============================")
  594. subprocess.check_call(["git", "submodule", "status", "--recursive"], cwd=sdk_path)
  595. print("==============================")
  596. print("| SDK: Modified files |")
  597. print("==============================")
  598. subprocess.check_call(["git", "status", "--short"], cwd=sdk_path)
  599. print("==============================")
  600. print("| Submodules: Modified files |")
  601. print("==============================")
  602. subprocess.check_call(["git", "submodule", "foreach", "--recursive", "git status --short"], cwd=sdk_path)
  603. print("Done.")
  604. # Don't block UI
  605. threading.Thread(target=print_version_info).start()
  606. return {"FINISHED"}
  607. class ArmAddonInstallButton(bpy.types.Operator):
  608. """Download and set up Armory SDK"""
  609. bl_idname = "arm_addon.install"
  610. bl_label = "Download and set up SDK"
  611. bl_description = "Download and set up the latest development version"
  612. def execute(self, context):
  613. download_sdk(self, context)
  614. return {"FINISHED"}
  615. class ArmAddonUpdateButton(bpy.types.Operator):
  616. """Update Armory SDK"""
  617. bl_idname = "arm_addon.update"
  618. bl_label = "Update SDK"
  619. bl_description = "Update to the latest development version"
  620. def execute(self, context):
  621. download_sdk(self, context)
  622. return {"FINISHED"}
  623. def download_sdk(self: bpy.types.Operator, context):
  624. global update_error_msg
  625. update_error_msg = ''
  626. sdk_path = get_sdk_path(context)
  627. if sdk_path == "":
  628. self.report({"ERROR"}, "Configure Armory SDK path first")
  629. return {"CANCELLED"}
  630. self.report({'INFO'}, 'Downloading Armory SDK, check console for details.')
  631. print('Armory (current add-on version' + str(bl_info['version']) + '): Cloning SDK repository recursively')
  632. if not os.path.exists(sdk_path):
  633. os.makedirs(sdk_path)
  634. if not git_test(self):
  635. return {"CANCELLED"}
  636. preferences = context.preferences
  637. addon_prefs = preferences.addons["armory"].preferences
  638. def done(failed: bool):
  639. if failed:
  640. self.report({"ERROR"}, "Failed updating the SDK submodules, check console for details.")
  641. else:
  642. update_armory_py(sdk_path)
  643. print('Armory SDK download completed, please restart Blender..')
  644. def done_clone(failed: bool):
  645. if failed:
  646. self.report({"ERROR"}, "Failed downloading Armory SDK, check console for details.")
  647. return
  648. if addon_prefs.update_submodules:
  649. if not git_test(self, (2, 34, 0)):
  650. # For some unknown (and seemingly not fixable) reason, git submodule update --remote
  651. # fails in earlier versions of Git with "fatal: Needed a single revision"
  652. done(True)
  653. else:
  654. prev_cwd = os.getcwd()
  655. os.chdir(sdk_path)
  656. run_proc(['git', 'submodule', 'update', '--remote', '--depth', '1', '--jobs', '4'], done)
  657. os.chdir(prev_cwd)
  658. else:
  659. done(False)
  660. git_clone(done_clone, sdk_path, 'armory3d/armsdk', '', recursive=True)
  661. class ArmAddonRestoreButton(bpy.types.Operator):
  662. """Update Armory SDK"""
  663. bl_idname = "arm_addon.restore"
  664. bl_label = "Restore SDK"
  665. bl_description = "Restore stable version"
  666. def execute(self, context):
  667. sdk_path = get_sdk_path(context)
  668. if sdk_path == "":
  669. self.report({"ERROR"}, "Configure Armory SDK path first")
  670. return {"CANCELLED"}
  671. restore_repo(sdk_path, '')
  672. self.report({'INFO'}, 'Restored stable version')
  673. return {"FINISHED"}
  674. class ArmAddonHelpButton(bpy.types.Operator):
  675. """Updater help"""
  676. bl_idname = "arm_addon.help"
  677. bl_label = "Help"
  678. bl_description = "Git is required for Armory Updater to work"
  679. def execute(self, context):
  680. webbrowser.open('https://github.com/armory3d/armory/wiki/gitversion')
  681. return {"FINISHED"}
  682. def update_armory_py(sdk_path: str, force_relink=False):
  683. """Ensure that armory.py is up to date by copying it from the
  684. current SDK path (if 'use_armory_py_symlink' is true, a symlink is
  685. created instead). Note that because the current version of armory.py
  686. is already loaded as a Python module, this change lags one add-on
  687. reload behind.
  688. """
  689. addon_prefs = ArmoryAddonPreferences.get_prefs()
  690. arm_module_file = Path(sys.modules['armory'].__file__)
  691. if addon_prefs.use_armory_py_symlink:
  692. if not arm_module_file.is_symlink() or force_relink:
  693. arm_module_file.unlink(missing_ok=True)
  694. try:
  695. arm_module_file.symlink_to(Path(sdk_path) / 'armory.py')
  696. except OSError as err:
  697. if hasattr(err, 'winerror'):
  698. if err.winerror == 1314: # ERROR_PRIVILEGE_NOT_HELD
  699. # Manually copy the file to "simulate" symlink
  700. shutil.copy(Path(sdk_path) / 'armory.py', arm_module_file)
  701. else:
  702. raise err
  703. else:
  704. raise err
  705. else:
  706. arm_module_file.unlink(missing_ok=True)
  707. shutil.copy(Path(sdk_path) / 'armory.py', arm_module_file)
  708. def start_armory(sdk_path: str):
  709. global is_running
  710. global last_scripts_path
  711. global last_sdk_path
  712. if sdk_path == "":
  713. return
  714. armory_path = os.path.join(sdk_path, "armory")
  715. if not os.path.exists(armory_path):
  716. print("Armory load error: 'armory' folder not found in SDK path."
  717. " Please make sure the SDK path is correct or that the SDK"
  718. " was downloaded correctly.")
  719. return
  720. apply_unix_permissions(sdk_path)
  721. scripts_path = os.path.join(armory_path, "blender")
  722. sys.path.append(scripts_path)
  723. last_scripts_path = scripts_path
  724. update_armory_py(sdk_path, force_relink=True)
  725. import start
  726. if last_sdk_path != "":
  727. import importlib
  728. start = importlib.reload(start)
  729. use_local_sdk = (sdk_source == SDKSource.LOCAL)
  730. start.register(local_sdk=use_local_sdk)
  731. last_sdk_path = sdk_path
  732. is_running = True
  733. print(f'Running Armory SDK from {sdk_path}')
  734. def stop_armory():
  735. global is_running
  736. if not is_running:
  737. return
  738. import start
  739. start.unregister()
  740. sys.path.remove(last_scripts_path)
  741. is_running = False
  742. def restart_armory(context):
  743. old_sdk_source = sdk_source
  744. sdk_path = get_sdk_path(context)
  745. if sdk_path == "":
  746. if not is_running:
  747. print("Configure Armory SDK path first")
  748. stop_armory()
  749. return
  750. # Only restart Armory when the SDK path changed or it isn't running,
  751. # otherwise we can keep the currently running instance
  752. if not same_path(last_sdk_path, sdk_path) or sdk_source != old_sdk_source or not is_running:
  753. stop_armory()
  754. assert not is_running
  755. start_armory(sdk_path)
  756. @persistent
  757. def on_load_post(context):
  758. restart_armory(bpy.context) # context is None, use bpy.context instead
  759. def on_register_post():
  760. detect_sdk_path()
  761. restart_armory(bpy.context)
  762. def register():
  763. bpy.utils.register_class(ArmoryAddonPreferences)
  764. bpy.utils.register_class(ArmAddonPrintVersionInfoButton)
  765. bpy.utils.register_class(ArmAddonInstallButton)
  766. bpy.utils.register_class(ArmAddonUpdateButton)
  767. bpy.utils.register_class(ArmAddonRestoreButton)
  768. bpy.utils.register_class(ArmAddonHelpButton)
  769. bpy.app.handlers.load_post.append(on_load_post)
  770. # Hack to avoid _RestrictContext
  771. bpy.app.timers.register(on_register_post, first_interval=0.01)
  772. def unregister():
  773. stop_armory()
  774. bpy.utils.unregister_class(ArmoryAddonPreferences)
  775. bpy.utils.unregister_class(ArmAddonInstallButton)
  776. bpy.utils.unregister_class(ArmAddonPrintVersionInfoButton)
  777. bpy.utils.unregister_class(ArmAddonUpdateButton)
  778. bpy.utils.unregister_class(ArmAddonRestoreButton)
  779. bpy.utils.unregister_class(ArmAddonHelpButton)
  780. bpy.app.handlers.load_post.remove(on_load_post)
  781. if __name__ == "__main__":
  782. register()