diff --git a/openpype/hosts/fusion/hooks/pre_fusion_setup.py b/openpype/hosts/fusion/hooks/pre_fusion_setup.py index 576628e876..3da8968727 100644 --- a/openpype/hosts/fusion/hooks/pre_fusion_setup.py +++ b/openpype/hosts/fusion/hooks/pre_fusion_setup.py @@ -64,5 +64,8 @@ class FusionPrelaunch(PreLaunchHook): self.launch_context.env[py3_var] = py3_dir + # for hook installing PySide2 + self.data["fusion_python3_home"] = py3_dir + self.log.info(f"Setting OPENPYPE_FUSION: {FUSION_HOST_DIR}") self.launch_context.env["OPENPYPE_FUSION"] = FUSION_HOST_DIR diff --git a/openpype/hosts/fusion/hooks/pre_pyside_install.py b/openpype/hosts/fusion/hooks/pre_pyside_install.py new file mode 100644 index 0000000000..f98aeda233 --- /dev/null +++ b/openpype/hosts/fusion/hooks/pre_pyside_install.py @@ -0,0 +1,186 @@ +import os +import subprocess +import platform +import uuid + +from openpype.lib.applications import PreLaunchHook, LaunchTypes + + +class InstallPySideToFusion(PreLaunchHook): + """Automatically installs Qt binding to fusion's python packages. + + Check if fusion has installed PySide2 and will try to install if not. + + For pipeline implementation is required to have Qt binding installed in + fusion's python packages. + """ + + app_groups = {"fusion"} + order = 2 + launch_types = {LaunchTypes.local} + + def execute(self): + # Prelaunch hook is not crucial + try: + settings = self.data["project_settings"][self.host_name] + if not settings["hooks"]["InstallPySideToFusion"]["enabled"]: + return + self.inner_execute() + except Exception: + self.log.warning( + "Processing of {} crashed.".format(self.__class__.__name__), + exc_info=True + ) + + def inner_execute(self): + self.log.debug("Check for PySide2 installation.") + + fusion_python3_home = self.data.get("fusion_python3_home") + if not fusion_python3_home: + self.log.warning("'fusion_python3_home' was not provided. " + "Installation of PySide2 not possible") + return + + if platform.system().lower() == "windows": + exe_filenames = ["python.exe"] + else: + exe_filenames = ["python3", "python"] + + for exe_filename in exe_filenames: + python_executable = os.path.join(fusion_python3_home, exe_filename) + if os.path.exists(python_executable): + break + + if not os.path.exists(python_executable): + self.log.warning( + "Couldn't find python executable for fusion. {}".format( + python_executable + ) + ) + return + + # Check if PySide2 is installed and skip if yes + if self._is_pyside_installed(python_executable): + self.log.debug("Fusion has already installed PySide2.") + return + + self.log.debug("Installing PySide2.") + # Install PySide2 in fusion's python + if self._windows_require_permissions( + os.path.dirname(python_executable)): + result = self._install_pyside_windows(python_executable) + else: + result = self._install_pyside(python_executable) + + if result: + self.log.info("Successfully installed PySide2 module to fusion.") + else: + self.log.warning("Failed to install PySide2 module to fusion.") + + def _install_pyside_windows(self, python_executable): + """Install PySide2 python module to fusion's python. + + Installation requires administration rights that's why it is required + to use "pywin32" module which can execute command's and ask for + administration rights. + """ + try: + import win32api + import win32con + import win32process + import win32event + import pywintypes + from win32comext.shell.shell import ShellExecuteEx + from win32comext.shell import shellcon + except Exception: + self.log.warning("Couldn't import \"pywin32\" modules") + return False + + try: + # Parameters + # - use "-m pip" as module pip to install PySide2 and argument + # "--ignore-installed" is to force install module to fusion's + # site-packages and make sure it is binary compatible + parameters = "-m pip install --ignore-installed PySide2" + + # Execute command and ask for administrator's rights + process_info = ShellExecuteEx( + nShow=win32con.SW_SHOWNORMAL, + fMask=shellcon.SEE_MASK_NOCLOSEPROCESS, + lpVerb="runas", + lpFile=python_executable, + lpParameters=parameters, + lpDirectory=os.path.dirname(python_executable) + ) + process_handle = process_info["hProcess"] + win32event.WaitForSingleObject(process_handle, + win32event.INFINITE) + returncode = win32process.GetExitCodeProcess(process_handle) + return returncode == 0 + except pywintypes.error: + return False + + def _install_pyside(self, python_executable): + """Install PySide2 python module to fusion's python.""" + try: + # Parameters + # - use "-m pip" as module pip to install PySide2 and argument + # "--ignore-installed" is to force install module to fusion's + # site-packages and make sure it is binary compatible + env = dict(os.environ) + del env['PYTHONPATH'] + args = [ + python_executable, + "-m", + "pip", + "install", + "--ignore-installed", + "PySide2", + ] + process = subprocess.Popen( + args, stdout=subprocess.PIPE, universal_newlines=True, + env=env + ) + process.communicate() + return process.returncode == 0 + except PermissionError: + self.log.warning( + "Permission denied with command:" + "\"{}\".".format(" ".join(args)) + ) + except OSError as error: + self.log.warning(f"OS error has occurred: \"{error}\".") + except subprocess.SubprocessError: + pass + + def _is_pyside_installed(self, python_executable): + """Check if PySide2 module is in fusion's pip list.""" + args = [python_executable, "-c", "from qtpy import QtWidgets"] + process = subprocess.Popen(args, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + _, stderr = process.communicate() + stderr = stderr.decode() + if stderr: + return False + return True + + def _windows_require_permissions(self, dirpath): + if platform.system().lower() != "windows": + return False + + try: + # Attempt to create a temporary file in the folder + temp_file_path = os.path.join(dirpath, uuid.uuid4().hex) + with open(temp_file_path, "w"): + pass + os.remove(temp_file_path) # Clean up temporary file + return False + + except PermissionError: + return True + + except BaseException as exc: + print(("Failed to determine if root requires permissions." + "Unexpected error: {}").format(exc)) + return False diff --git a/openpype/settings/defaults/project_settings/fusion.json b/openpype/settings/defaults/project_settings/fusion.json index 0edcae060a..8579442625 100644 --- a/openpype/settings/defaults/project_settings/fusion.json +++ b/openpype/settings/defaults/project_settings/fusion.json @@ -15,6 +15,11 @@ "copy_status": false, "force_sync": false }, + "hooks": { + "InstallPySideToFusion": { + "enabled": true + } + }, "create": { "CreateSaver": { "temp_rendering_path_template": "{workdir}/renders/fusion/{subset}/{subset}.{frame}.{ext}", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json b/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json index 5177d8bc7c..fbd856b895 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json @@ -41,6 +41,29 @@ } ] }, + { + "type": "dict", + "collapsible": true, + "key": "hooks", + "label": "Hooks", + "children": [ + { + "type": "dict", + "collapsible": true, + "checkbox_key": "enabled", + "key": "InstallPySideToFusion", + "label": "Install PySide2", + "is_group": true, + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + } + ] + } + ] + }, { "type": "dict", "collapsible": true, diff --git a/server_addon/fusion/server/settings.py b/server_addon/fusion/server/settings.py index 1bc12773d2..21189b390e 100644 --- a/server_addon/fusion/server/settings.py +++ b/server_addon/fusion/server/settings.py @@ -25,16 +25,6 @@ def _create_saver_instance_attributes_enum(): ] -def _image_format_enum(): - return [ - {"value": "exr", "label": "exr"}, - {"value": "tga", "label": "tga"}, - {"value": "png", "label": "png"}, - {"value": "tif", "label": "tif"}, - {"value": "jpg", "label": "jpg"}, - ] - - class CreateSaverPluginModel(BaseSettingsModel): _isGroup = True temp_rendering_path_template: str = Field( @@ -49,9 +39,23 @@ class CreateSaverPluginModel(BaseSettingsModel): enum_resolver=_create_saver_instance_attributes_enum, title="Instance attributes" ) - image_format: str = Field( - enum_resolver=_image_format_enum, - title="Output Image Format" + output_formats: list[str] = Field( + default_factory=list, + title="Output formats" + ) + + +class HookOptionalModel(BaseSettingsModel): + enabled: bool = Field( + True, + title="Enabled" + ) + + +class HooksModel(BaseSettingsModel): + InstallPySideToFusion: HookOptionalModel = Field( + default_factory=HookOptionalModel, + title="Install PySide2" ) @@ -71,6 +75,10 @@ class FusionSettings(BaseSettingsModel): default_factory=CopyFusionSettingsModel, title="Local Fusion profile settings" ) + hooks: HooksModel = Field( + default_factory=HooksModel, + title="Hooks" + ) create: CreatPluginsModel = Field( default_factory=CreatPluginsModel, title="Creator plugins" @@ -93,6 +101,11 @@ DEFAULT_VALUES = { "copy_status": False, "force_sync": False }, + "hooks": { + "InstallPySideToFusion": { + "enabled": True + } + }, "create": { "CreateSaver": { "temp_rendering_path_template": "{workdir}/renders/fusion/{product[name]}/{product[name]}.{frame}.{ext}", @@ -104,7 +117,15 @@ DEFAULT_VALUES = { "reviewable", "farm_rendering" ], - "image_format": "exr" + "output_formats": [ + "exr", + "jpg", + "jpeg", + "jpg", + "tiff", + "png", + "tga" + ] } } } diff --git a/server_addon/fusion/server/version.py b/server_addon/fusion/server/version.py index 485f44ac21..b3f4756216 100644 --- a/server_addon/fusion/server/version.py +++ b/server_addon/fusion/server/version.py @@ -1 +1 @@ -__version__ = "0.1.1" +__version__ = "0.1.2"