poly-hammer-utils 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- poly_hammer_utils-0.0.1/PKG-INFO +19 -0
- poly_hammer_utils-0.0.1/README.md +6 -0
- poly_hammer_utils-0.0.1/pyproject.toml +16 -0
- poly_hammer_utils-0.0.1/src/poly_hammer_utils/__init__.py +0 -0
- poly_hammer_utils-0.0.1/src/poly_hammer_utils/addon/packager.py +229 -0
- poly_hammer_utils-0.0.1/src/poly_hammer_utils/constants.py +5 -0
- poly_hammer_utils-0.0.1/src/poly_hammer_utils/helpers.py +66 -0
- poly_hammer_utils-0.0.1/src/poly_hammer_utils/launch.py +21 -0
- poly_hammer_utils-0.0.1/src/poly_hammer_utils/resources/addon-requirements/requirements.txt +1 -0
- poly_hammer_utils-0.0.1/src/poly_hammer_utils/resources/scripts/blender/startup.py +45 -0
- poly_hammer_utils-0.0.1/src/poly_hammer_utils/resources/scripts/unreal/init_unreal.py +22 -0
- poly_hammer_utils-0.0.1/src/poly_hammer_utils/utilities.py +109 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: poly-hammer-utils
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary:
|
|
5
|
+
Author: Poly Hammer
|
|
6
|
+
Author-email: info@polyhammer.com
|
|
7
|
+
Requires-Python: >=3.11,<4.0
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Requires-Dist: requirements-parser (>=0.11.0,<0.12.0)
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# Poly Hammer Utils
|
|
15
|
+
|
|
16
|
+
[](https://github.com/poly-hammer/poly-hammer-utils/actions/workflows/publish.yaml)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
A general purpose package that holds our dev utilities.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Poly Hammer Utils
|
|
2
|
+
|
|
3
|
+
[](https://github.com/poly-hammer/poly-hammer-utils/actions/workflows/publish.yaml)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
A general purpose package that holds our dev utilities.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "poly-hammer-utils"
|
|
3
|
+
version = "0.0.1"
|
|
4
|
+
description = ""
|
|
5
|
+
authors = ["Poly Hammer <info@polyhammer.com>"]
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
packages = [{include = "poly_hammer_utils", from = "src"}]
|
|
8
|
+
|
|
9
|
+
[tool.poetry.dependencies]
|
|
10
|
+
python = "^3.11"
|
|
11
|
+
requirements-parser = "^0.11.0"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
[build-system]
|
|
15
|
+
requires = ["poetry-core"]
|
|
16
|
+
build-backend = "poetry.core.masonry.api"
|
|
File without changes
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import ast
|
|
3
|
+
import sys
|
|
4
|
+
import shutil
|
|
5
|
+
import logging
|
|
6
|
+
import subprocess
|
|
7
|
+
import requirements
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
import bpy
|
|
12
|
+
import addon_utils
|
|
13
|
+
except ImportError:
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
REPO_ROOT = Path(__file__).parent.parent.parent.parent.parent
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
IGNORE_PATTERNS = [
|
|
20
|
+
"__pycache__",
|
|
21
|
+
"*.pyc"
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class AddonPackager:
|
|
26
|
+
|
|
27
|
+
def __init__(self, addon_name, addon_folder_path, output_folder):
|
|
28
|
+
"""
|
|
29
|
+
Initializes the class and sets the addon module name.
|
|
30
|
+
"""
|
|
31
|
+
self.addon_name = addon_name
|
|
32
|
+
self.addon_folder_path = addon_folder_path
|
|
33
|
+
self.output_folder = output_folder
|
|
34
|
+
|
|
35
|
+
@staticmethod
|
|
36
|
+
def get_dict_from_python_file(python_file, dict_name):
|
|
37
|
+
"""
|
|
38
|
+
Gets the first dictionary from the given file that matches the given variable name.
|
|
39
|
+
|
|
40
|
+
:param object python_file: A file object to read from.
|
|
41
|
+
:param str dict_name: The variable name of a dictionary.
|
|
42
|
+
:return dict: The value of the dictionary.
|
|
43
|
+
"""
|
|
44
|
+
dictionary = {}
|
|
45
|
+
tree = ast.parse(python_file.read())
|
|
46
|
+
|
|
47
|
+
for item in tree.body:
|
|
48
|
+
if hasattr(item, 'targets'):
|
|
49
|
+
for target in item.targets:
|
|
50
|
+
if getattr(target, 'id', None) == dict_name:
|
|
51
|
+
for index, key in enumerate(item.value.keys):
|
|
52
|
+
# add string as dictionary value
|
|
53
|
+
if hasattr(item.value.values[index], 's'):
|
|
54
|
+
dictionary[key.s] = item.value.values[index].s
|
|
55
|
+
|
|
56
|
+
# add number as dictionary value
|
|
57
|
+
elif hasattr(item.value.values[index], 'n'):
|
|
58
|
+
dictionary[key.s] = item.value.values[index].n
|
|
59
|
+
|
|
60
|
+
# add list as dictionary value
|
|
61
|
+
elif hasattr(item.value.values[index], 'elts'):
|
|
62
|
+
list_value = []
|
|
63
|
+
for element in item.value.values[index].elts:
|
|
64
|
+
# add a number to the list
|
|
65
|
+
if hasattr(element, 'n'):
|
|
66
|
+
list_value.append(element.n)
|
|
67
|
+
|
|
68
|
+
# add a string to the list
|
|
69
|
+
elif hasattr(element, 's'):
|
|
70
|
+
list_value.append(element.s)
|
|
71
|
+
|
|
72
|
+
dictionary[key.s] = list_value
|
|
73
|
+
break
|
|
74
|
+
return dictionary
|
|
75
|
+
|
|
76
|
+
def get_addon_version_number(self, addon_folder_path):
|
|
77
|
+
"""
|
|
78
|
+
Gets the version number from the addons bl_info
|
|
79
|
+
|
|
80
|
+
:param str addon_folder_path: The path to the addon folder.
|
|
81
|
+
:return str: The version of the addon.
|
|
82
|
+
"""
|
|
83
|
+
addon_init = open(os.path.join(addon_folder_path, '__init__.py'))
|
|
84
|
+
|
|
85
|
+
bl_info = self.get_dict_from_python_file(addon_init, 'bl_info')
|
|
86
|
+
version_numbers = [str(number) for number in bl_info['version']]
|
|
87
|
+
version_number = '.'.join(version_numbers)
|
|
88
|
+
return version_number
|
|
89
|
+
|
|
90
|
+
def get_addon_zip_path(self):
|
|
91
|
+
"""
|
|
92
|
+
Gets the path to the addons zip file.
|
|
93
|
+
|
|
94
|
+
:return str: The full path to the released zip file.
|
|
95
|
+
"""
|
|
96
|
+
# get the versioned addon paths
|
|
97
|
+
version_number = self.get_addon_version_number(self.addon_folder_path)
|
|
98
|
+
addon_zip_file = f'{self.addon_name}_{version_number}.zip'
|
|
99
|
+
output_folder_path = os.path.join(self.output_folder, addon_zip_file)
|
|
100
|
+
if not os.path.exists(os.path.dirname(output_folder_path)):
|
|
101
|
+
os.makedirs(os.path.dirname(output_folder_path))
|
|
102
|
+
|
|
103
|
+
return output_folder_path
|
|
104
|
+
|
|
105
|
+
@staticmethod
|
|
106
|
+
def set_folder_contents_permissions(folder_path, permission_level):
|
|
107
|
+
"""
|
|
108
|
+
Goes through all files and folders contained in the folder and modifies their permissions to
|
|
109
|
+
the given permissions.
|
|
110
|
+
|
|
111
|
+
:param str folder_path: The full path to the folder you would like to modify permissions on.
|
|
112
|
+
:param octal permission_level: The octal permissions value.
|
|
113
|
+
"""
|
|
114
|
+
for root, directories, files in os.walk(folder_path):
|
|
115
|
+
for directory in directories:
|
|
116
|
+
os.chmod(os.path.join(root, directory), permission_level)
|
|
117
|
+
for file in files:
|
|
118
|
+
os.chmod(os.path.join(root, file), permission_level)
|
|
119
|
+
|
|
120
|
+
def copy_addon(self):
|
|
121
|
+
"""
|
|
122
|
+
Copy the addon.
|
|
123
|
+
"""
|
|
124
|
+
destination = os.path.join(self.output_folder, self.addon_name)
|
|
125
|
+
|
|
126
|
+
logging.debug(f'Copying addon "{self.addon_folder_path}" to "{destination}"')
|
|
127
|
+
|
|
128
|
+
# change the permissions to allow the folders contents to be modified.
|
|
129
|
+
if sys.platform == 'win32':
|
|
130
|
+
self.set_folder_contents_permissions(os.path.join(self.addon_folder_path, os.pardir), 0o777)
|
|
131
|
+
|
|
132
|
+
shutil.rmtree(destination, ignore_errors=True)
|
|
133
|
+
shutil.copytree(self.addon_folder_path, destination, ignore=shutil.ignore_patterns('*pyc'))
|
|
134
|
+
|
|
135
|
+
# change the permissions to allow the folders contents to be modified.
|
|
136
|
+
if sys.platform == 'win32':
|
|
137
|
+
self.set_folder_contents_permissions(os.path.join(self.output_folder, os.pardir), 0o777)
|
|
138
|
+
|
|
139
|
+
def zip_addon(self):
|
|
140
|
+
"""
|
|
141
|
+
Zips up the addon.
|
|
142
|
+
"""
|
|
143
|
+
logging.debug(f'zipping addon "{self.addon_name}" to "{self.output_folder}"')
|
|
144
|
+
# get the folder paths
|
|
145
|
+
versioned_zip_file_path = self.get_addon_zip_path()
|
|
146
|
+
versioned_folder_path = versioned_zip_file_path.replace('.zip', '')
|
|
147
|
+
|
|
148
|
+
# change the permissions to allow the folders contents to be modified.
|
|
149
|
+
if sys.platform == 'win32':
|
|
150
|
+
self.set_folder_contents_permissions(os.path.join(self.addon_folder_path, os.pardir), 0o777)
|
|
151
|
+
|
|
152
|
+
# remove the existing zip archive
|
|
153
|
+
if os.path.exists(versioned_zip_file_path):
|
|
154
|
+
try:
|
|
155
|
+
os.remove(versioned_zip_file_path)
|
|
156
|
+
except PermissionError:
|
|
157
|
+
logging.warning(f'Could not delete {versioned_folder_path}!')
|
|
158
|
+
|
|
159
|
+
# copy the addon module in to the versioned directory with its addon module name as a sub directory
|
|
160
|
+
shutil.copytree(
|
|
161
|
+
self.addon_folder_path,
|
|
162
|
+
os.path.join(versioned_folder_path, self.addon_name),
|
|
163
|
+
ignore=shutil.ignore_patterns(*IGNORE_PATTERNS)
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
# make a zip archive of the copied folder
|
|
167
|
+
shutil.make_archive(versioned_folder_path, 'zip', versioned_folder_path)
|
|
168
|
+
|
|
169
|
+
# remove the copied directory
|
|
170
|
+
shutil.rmtree(versioned_folder_path)
|
|
171
|
+
|
|
172
|
+
# return the full path to the zip file
|
|
173
|
+
return versioned_zip_file_path
|
|
174
|
+
|
|
175
|
+
def install_addon(self):
|
|
176
|
+
"""
|
|
177
|
+
Installs the given addons from the release folder.
|
|
178
|
+
"""
|
|
179
|
+
zip_file_path = self.get_addon_zip_path()
|
|
180
|
+
|
|
181
|
+
# install addon if it isn't installed
|
|
182
|
+
if not self.is_addon_installed(self.addon_name):
|
|
183
|
+
bpy.ops.preferences.addon_install(filepath=zip_file_path)
|
|
184
|
+
|
|
185
|
+
# enable addon if it is disabled
|
|
186
|
+
if self.addon_name not in bpy.context.preferences.addons.keys():
|
|
187
|
+
bpy.ops.preferences.addon_enable(module=self.addon_name)
|
|
188
|
+
|
|
189
|
+
@staticmethod
|
|
190
|
+
def is_addon_installed(module_name):
|
|
191
|
+
"""
|
|
192
|
+
Checks if the given addon module is currently installed.
|
|
193
|
+
|
|
194
|
+
:param str module_name: The addon module name.
|
|
195
|
+
:return bool: True or False depending on whether the addon exists.
|
|
196
|
+
"""
|
|
197
|
+
return module_name in [module.bl_info.get('name') for module in addon_utils.modules()]
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def install_package(package):
|
|
201
|
+
subprocess.check_call([
|
|
202
|
+
sys.executable,
|
|
203
|
+
"-m",
|
|
204
|
+
"pip",
|
|
205
|
+
"install",
|
|
206
|
+
"--upgrade",
|
|
207
|
+
"--target=./src/addons/metahuman/resources/packages",
|
|
208
|
+
package
|
|
209
|
+
],
|
|
210
|
+
cwd=REPO_ROOT
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def install_requirements():
|
|
215
|
+
requirements_file_path = os.path.join(os.path.dirname(__file__), 'requirements.txt')
|
|
216
|
+
with open(requirements_file_path, 'r') as requirements_file:
|
|
217
|
+
for requirement in requirements.parse(requirements_file):
|
|
218
|
+
versioned_package = f'{requirement.name}{"".join(requirement.specs[0])}'
|
|
219
|
+
install_package(versioned_package)
|
|
220
|
+
|
|
221
|
+
if __name__ == "__main__":
|
|
222
|
+
addon_packager = AddonPackager(
|
|
223
|
+
addon_name='metahuman',
|
|
224
|
+
addon_folder_path=str(REPO_ROOT / 'src' / 'addons' / 'metahuman'),
|
|
225
|
+
output_folder=str(REPO_ROOT / 'release')
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
addon_packager.zip_addon()
|
|
229
|
+
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import bpy
|
|
3
|
+
import sys
|
|
4
|
+
import importlib
|
|
5
|
+
from types import ModuleType
|
|
6
|
+
repo_folder = os.path.join(os.path.dirname(__file__), os.pardir)
|
|
7
|
+
# from addon_packager import AddonPackager
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def deep_reload(m: ModuleType):
|
|
11
|
+
name = m.__name__ # get the name that is used in sys.modules
|
|
12
|
+
name_ext = name + '.' # support finding sub modules or packages
|
|
13
|
+
|
|
14
|
+
def compare(loaded: str):
|
|
15
|
+
return (loaded == name) or loaded.startswith(name_ext)
|
|
16
|
+
|
|
17
|
+
all_mods = tuple(sys.modules) # prevent changing iterable while iterating over it
|
|
18
|
+
sub_mods = filter(compare, all_mods)
|
|
19
|
+
for pkg in sorted(sub_mods, key=lambda item: item.count('.'), reverse=True):
|
|
20
|
+
importlib.reload(sys.modules[pkg]) # reload packages, beginning with the most deeply nested
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def reload_addon_source_code(addons, only_unregister=False):
|
|
24
|
+
""""
|
|
25
|
+
Does a full reload of the addons directly from their source code in the repo. This
|
|
26
|
+
tends to be the preferred method of working since stack traces will link back to the
|
|
27
|
+
source code.
|
|
28
|
+
|
|
29
|
+
:param list addons: A list of addon names.
|
|
30
|
+
:param bool only_unregister: Whether or not to only unregister the addon code.
|
|
31
|
+
"""
|
|
32
|
+
# forces reloading of modules, regeneration of properties, and sends all errors
|
|
33
|
+
# to stderr instead of a dialog
|
|
34
|
+
for addon in addons:
|
|
35
|
+
os.environ[f'{addon.upper()}_DEV'] = '1'
|
|
36
|
+
addon = importlib.import_module(addon)
|
|
37
|
+
|
|
38
|
+
addon.unregister()
|
|
39
|
+
deep_reload(addon)
|
|
40
|
+
# importlib.reload(addon)
|
|
41
|
+
|
|
42
|
+
if not only_unregister:
|
|
43
|
+
addon.register()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def reload_addon_zips(addons):
|
|
47
|
+
"""
|
|
48
|
+
Does a full install and reload of the addons from their zip files. This is useful when
|
|
49
|
+
testing the full addon installation, and when you need to see the addon preferences UI which
|
|
50
|
+
is only available when a addon is physically installed.
|
|
51
|
+
|
|
52
|
+
:param list addons: A list of addon names.
|
|
53
|
+
"""
|
|
54
|
+
# unregister any registered addons
|
|
55
|
+
reload_addon_source_code(addons, only_unregister=True)
|
|
56
|
+
|
|
57
|
+
# zip up each addon
|
|
58
|
+
for addon in addons:
|
|
59
|
+
os.environ[f'{addon.upper()}_DEV'] = '1'
|
|
60
|
+
addon_folder_path = os.path.join(repo_folder, addon)
|
|
61
|
+
release_folder_path = os.path.join(repo_folder, 'release')
|
|
62
|
+
addon_packager = AddonPackager(addon, addon_folder_path, release_folder_path)
|
|
63
|
+
addon_packager.zip_addon()
|
|
64
|
+
addon_packager.install_addon()
|
|
65
|
+
|
|
66
|
+
bpy.ops.script.reload()
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from .utilities import launch_blender, launch_unreal
|
|
3
|
+
app_name = sys.argv[1]
|
|
4
|
+
app_version = sys.argv[2]
|
|
5
|
+
debug_on = sys.argv[3]
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
if __name__ == '__main__':
|
|
9
|
+
debug_on = '1' if debug_on.lower() == 'yes' else '0'
|
|
10
|
+
|
|
11
|
+
if app_name == 'blender':
|
|
12
|
+
launch_blender(
|
|
13
|
+
version=app_version,
|
|
14
|
+
debug=debug_on
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
if app_name == 'unreal':
|
|
18
|
+
launch_unreal(
|
|
19
|
+
version=app_version,
|
|
20
|
+
debug=debug_on
|
|
21
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
sentry-sdk==1.38.0
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import bpy
|
|
4
|
+
import logging
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
10
|
+
|
|
11
|
+
if int(os.environ.get('BLENDER_DEBUGGING_ON', '0')):
|
|
12
|
+
try:
|
|
13
|
+
import debugpy
|
|
14
|
+
port = int(os.environ.get('BLENDER_DEBUG_PORT', 5678))
|
|
15
|
+
debugpy.configure(python=sys.executable)
|
|
16
|
+
debugpy.listen(port)
|
|
17
|
+
logger.info(f'Waiting for debugger to attach on port {port}...')
|
|
18
|
+
debugpy.wait_for_client()
|
|
19
|
+
except ImportError:
|
|
20
|
+
logger.error(
|
|
21
|
+
'Failed to initialize debugger because debugpy is not available '
|
|
22
|
+
'in the current python environment.'
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
BLENDER_SCRIPTS_FOLDERS = [Path(i) for i in os.environ.get('BLENDER_SCRIPTS_FOLDERS', '').split(os.pathsep)]
|
|
26
|
+
|
|
27
|
+
for scripts_folder in BLENDER_SCRIPTS_FOLDERS:
|
|
28
|
+
script_directory = bpy.context.preferences.filepaths.script_directories.get(scripts_folder.parent.name)
|
|
29
|
+
if script_directory:
|
|
30
|
+
bpy.context.preferences.filepaths.script_directories.remove(script_directory)
|
|
31
|
+
|
|
32
|
+
script_directory = bpy.context.preferences.filepaths.script_directories.new()
|
|
33
|
+
script_directory.name = scripts_folder.parent.name
|
|
34
|
+
script_directory.directory = str(scripts_folder)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
bpy.ops.script.reload()
|
|
39
|
+
except ValueError:
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
for scripts_folder in BLENDER_SCRIPTS_FOLDERS:
|
|
43
|
+
for addon in os.listdir(scripts_folder / 'addons'):
|
|
44
|
+
if (scripts_folder / 'addons' / addon).is_dir():
|
|
45
|
+
bpy.ops.preferences.addon_enable(module=addon)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import logging
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
logger = logging.getLogger(__name__)
|
|
7
|
+
|
|
8
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
if int(os.environ.get('UNREAL_DEBUGGING_ON', '0')):
|
|
12
|
+
import debugpy
|
|
13
|
+
platform_folder = 'Linux'
|
|
14
|
+
if sys.platform == 'win32':
|
|
15
|
+
platform_folder = 'Win64'
|
|
16
|
+
|
|
17
|
+
port = int(os.environ.get('UNREAL_DEBUG_PORT', 5678))
|
|
18
|
+
python_exe_path = Path(sys.executable).parent.parent / 'ThirdParty' / 'Python3' / platform_folder / 'python'
|
|
19
|
+
debugpy.configure(python=os.environ.get('UNREAL_PYTHON_EXE', str(python_exe_path)))
|
|
20
|
+
debugpy.listen(port)
|
|
21
|
+
logger.info(f'Waiting for debugger to attach on port {port}...')
|
|
22
|
+
debugpy.wait_for_client()
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import site
|
|
4
|
+
import subprocess
|
|
5
|
+
import poly_hammer_utils
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from poly_hammer_utils.constants import BLENDER_STARTUP_SCRIPT
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
REPO_ROOT = Path(__file__).parent.parent.parent.parent
|
|
11
|
+
UNREAL_PROJECT = Path(os.environ['UNREAL_PROJECT'])
|
|
12
|
+
UNREAL_EXE = os.environ.get('UNREAL_EXE')
|
|
13
|
+
UNREAL_STARTUP_SCRIPT = Path(__file__).parent / 'resources' / 'scripts' / 'unreal' / 'init_unreal.py'
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def shell(command: str, **kwargs):
|
|
17
|
+
"""
|
|
18
|
+
Runs the command is a fully qualified shell.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
command (str): A command.
|
|
22
|
+
|
|
23
|
+
Raises:
|
|
24
|
+
OSError: The error cause by the shell.
|
|
25
|
+
"""
|
|
26
|
+
process = subprocess.Popen(
|
|
27
|
+
command,
|
|
28
|
+
shell=True,
|
|
29
|
+
universal_newlines=True,
|
|
30
|
+
stdout=subprocess.PIPE,
|
|
31
|
+
stderr=subprocess.STDOUT,
|
|
32
|
+
**kwargs
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
output = []
|
|
36
|
+
for line in iter(process.stdout.readline, ""): # type: ignore
|
|
37
|
+
output += [line.rstrip()]
|
|
38
|
+
sys.stdout.write(line)
|
|
39
|
+
|
|
40
|
+
process.wait()
|
|
41
|
+
|
|
42
|
+
if process.returncode != 0:
|
|
43
|
+
raise OSError("\n".join(output))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def launch_blender(version: str, debug: str):
|
|
47
|
+
if sys.platform == 'win32':
|
|
48
|
+
exe_path = rf"C:\Program Files\Blender Foundation\Blender {version}\blender.exe"
|
|
49
|
+
else:
|
|
50
|
+
exe_path = None
|
|
51
|
+
|
|
52
|
+
if exe_path:
|
|
53
|
+
command = f'"{exe_path}" --python-use-system-env --python "{BLENDER_STARTUP_SCRIPT}"'
|
|
54
|
+
shell(
|
|
55
|
+
command,
|
|
56
|
+
env={
|
|
57
|
+
**os.environ.copy(),
|
|
58
|
+
'PYTHONUNBUFFERED': '1',
|
|
59
|
+
'BLENDER_APP_VERSION': version,
|
|
60
|
+
'BLENDER_DEBUGGING_ON': debug,
|
|
61
|
+
'PYTHONPATH': ';'.join(
|
|
62
|
+
site.getsitepackages() + [str(Path(poly_hammer_utils.__file__).parent.parent)]
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def launch_unreal(version: str, debug: str):
|
|
69
|
+
if sys.platform == 'win32':
|
|
70
|
+
exe_path = rf'C:\Program Files\Epic Games\UE_{version}\Engine\Binaries\Win64\UnrealEditor.exe'
|
|
71
|
+
else:
|
|
72
|
+
exe_path = None
|
|
73
|
+
|
|
74
|
+
if UNREAL_EXE:
|
|
75
|
+
exe_path = UNREAL_EXE
|
|
76
|
+
|
|
77
|
+
if exe_path:
|
|
78
|
+
command = f'"{exe_path}" "{UNREAL_PROJECT}" -stdout -nopause -forcelogflush -verbose'
|
|
79
|
+
shell(
|
|
80
|
+
command,
|
|
81
|
+
env={
|
|
82
|
+
**os.environ.copy(),
|
|
83
|
+
'UNREAL_APP_VERSION': version,
|
|
84
|
+
'UNREAL_DEBUGGING_ON': debug,
|
|
85
|
+
'UE_PYTHONPATH': ';'.join([
|
|
86
|
+
*site.getsitepackages(),
|
|
87
|
+
str(Path(poly_hammer_utils.__file__).parent.parent.absolute()),
|
|
88
|
+
str(UNREAL_STARTUP_SCRIPT.parent.absolute())
|
|
89
|
+
])
|
|
90
|
+
}
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
if __name__ == "__main__":
|
|
95
|
+
app_name = sys.argv[1]
|
|
96
|
+
app_version = sys.argv[2]
|
|
97
|
+
debug_on = sys.argv[3]
|
|
98
|
+
|
|
99
|
+
if app_name == 'blender':
|
|
100
|
+
launch_blender(
|
|
101
|
+
version=app_version,
|
|
102
|
+
debug=debug_on
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
if app_name == 'unreal':
|
|
106
|
+
launch_unreal(
|
|
107
|
+
version=app_version,
|
|
108
|
+
debug=debug_on
|
|
109
|
+
)
|