From ba1a922470f529209a4b7481b15b5bd62ff4fd36 Mon Sep 17 00:00:00 2001 From: Evan Pratten Date: Thu, 5 Oct 2023 19:33:00 -0400 Subject: [PATCH] Add tools for launching lightly toasted Houdini --- .gitignore | 3 +- .vscode/settings.json | 8 +- configs/houdini19.5/scripts/456.cmd | 1 + configs/scripts/houdini-tool.py | 106 ++++++++++++++ configs/scripts/hython-latest | 9 ++ configs/scripts/run-logid | 0 configs/scripts/usdnc-to-usd.py | 47 +++++++ configs/ssh/config | 2 + install.conf.yaml | 8 +- python_modules/ewpipe/common/dirs.py | 13 ++ python_modules/ewpipe/common/env.py | 31 +++++ python_modules/ewpipe/common/logging.py | 8 ++ python_modules/ewpipe/houdini/editions.py | 36 +++++ .../ewpipe/houdini/installations.py | 129 ++++++++++++++++++ 14 files changed, 395 insertions(+), 6 deletions(-) create mode 100644 configs/houdini19.5/scripts/456.cmd create mode 100755 configs/scripts/houdini-tool.py create mode 100755 configs/scripts/hython-latest mode change 100644 => 100755 configs/scripts/run-logid create mode 100755 configs/scripts/usdnc-to-usd.py create mode 100644 python_modules/ewpipe/common/dirs.py create mode 100644 python_modules/ewpipe/common/env.py create mode 100644 python_modules/ewpipe/common/logging.py create mode 100644 python_modules/ewpipe/houdini/editions.py create mode 100644 python_modules/ewpipe/houdini/installations.py diff --git a/.gitignore b/.gitignore index ef07850..4fb0350 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -/configs/remmina \ No newline at end of file +/configs/remmina +__pycache__ \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 58d16ed..270f0cd 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,6 +5,10 @@ "*.md.liquid": "markdown", "*.js.liquid": "liquid-javascript", "*.css.liquid": "liquid-css", - "*.scss.liquid": "liquid-scss", - } + "*.scss.liquid": "liquid-scss" + }, + // Add python_modules to the python path + "python.analysis.extraPaths": [ + "./python_modules" + ], } \ No newline at end of file diff --git a/configs/houdini19.5/scripts/456.cmd b/configs/houdini19.5/scripts/456.cmd new file mode 100644 index 0000000..4101639 --- /dev/null +++ b/configs/houdini19.5/scripts/456.cmd @@ -0,0 +1 @@ +preference general.desk.val "Solaris" \ No newline at end of file diff --git a/configs/scripts/houdini-tool.py b/configs/scripts/houdini-tool.py new file mode 100755 index 0000000..a2a0dec --- /dev/null +++ b/configs/scripts/houdini-tool.py @@ -0,0 +1,106 @@ +#! /usr/bin/env python3 + +# fmt:off +import sys +import os +from pathlib import Path +sys.path.append((Path(os.environ["EWCONFIG_ROOT"]) / "python_modules").as_posix()) +# fmt:on + +import argparse +import subprocess +import logging +from ewpipe.common.dirs import HOUDINI_PROJECTS_DIR +from ewpipe.houdini.editions import ( + get_binary_name_for_edition, + get_houdini_edition_args, + HOU_EDITIONS, +) +from ewpipe.houdini.installations import get_houdini_installation_path +from ewpipe.common.logging import configure_logging +from ewpipe.common.env import diff_from_current_env + +logger = logging.getLogger(__name__) + + +def main() -> int: + # Handle program arguments + ap = argparse.ArgumentParser( + prog="houdini-tool", + description="Evan's tool for launching and managing Houdini", + ) + ap.add_argument( + "--type", + "-t", + help="Houdini type", + choices=HOU_EDITIONS, + default="apprentice", + ) + ap.add_argument( + "--project", + "-p", + help="Name of the project to open or create. May also be a direct path", + type=str, + required=True, + ) + ap.add_argument( + "--hou-version", + help="Houdini version to use. Defaults to latest", + type=str, + default=None, + ) + ap.add_argument( + "--no-project-env", help="Disables setting $HIP and $JOB", action="store_true" + ) + ap.add_argument("--verbose", "-v", help="Verbose output", action="store_true") + args = ap.parse_args() + + # Set up verbose logging if requested + configure_logging(verbose=args.verbose) + + # Get the houdini path + hou_path = get_houdini_installation_path(version=args.hou_version) + if not hou_path: + logger.error("Could not find Houdini installation") + return 1 + logger.info(f"Selected Houdini {hou_path.name[3:]} from {hou_path}") + + # Determine the project path + project_path = Path(args.project) + if not project_path.is_absolute(): + # This is a project name, not a path + project_path = HOUDINI_PROJECTS_DIR / project_path + logger.info(f"Opening project from: {project_path}") + + # If the directory does not exist, create + project_path.mkdir(parents=True, exist_ok=True) + + # If allowed, set up env vars + environment_vars = os.environ.copy() + environment_vars["HOUDINI_SCRIPT_DEBUG"] = "1" + environment_vars["HOUDINI_SPLASH_MESSAGE"] = "Loading with custom scripts" + environment_vars["HOUDINI_CONSOLE_PYTHON_PANEL_ERROR"] = "1" + if not args.no_project_env: + # environment_vars["HIP"] = str(project_path) + environment_vars["JOB"] = str(project_path) + environment_vars["HOUDINI_HIP_DEFAULT_NAME"] = f"{project_path.name}.hip" + + # Figure out what has changed in the environment and print the changes + env_changes = diff_from_current_env(environment_vars) + if env_changes: + logger.info("Environment changes:") + for key, value in env_changes.items(): + logger.info(f" ${key}: {value}") + + # Launch houdini + cmd = [ + str(hou_path / "bin" / get_binary_name_for_edition(args.type)), + "-foreground", + ] + get_houdini_edition_args(args.type) + logger.info(f"Running: {' '.join(cmd)}") + status = subprocess.run(cmd, env=environment_vars, cwd=project_path).returncode + return status + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/configs/scripts/hython-latest b/configs/scripts/hython-latest new file mode 100755 index 0000000..205e7ca --- /dev/null +++ b/configs/scripts/hython-latest @@ -0,0 +1,9 @@ +#! /bin/bash +set -e + +# Find hython +HOUDINI_PATH=`python3 ~/.config/ewconfig/python_modules/ewpipe/houdini/installations.py` +HYTHON_PATH=$HOUDINI_PATH/bin/hython + +# Execute hython, passing through all arguments +$HYTHON_PATH $@ \ No newline at end of file diff --git a/configs/scripts/run-logid b/configs/scripts/run-logid old mode 100644 new mode 100755 diff --git a/configs/scripts/usdnc-to-usd.py b/configs/scripts/usdnc-to-usd.py new file mode 100755 index 0000000..9696096 --- /dev/null +++ b/configs/scripts/usdnc-to-usd.py @@ -0,0 +1,47 @@ +#! /usr/bin/env -S hython-latest -I +import argparse +import sys +from pxr import Usd +from pathlib import Path + + +def main() -> int: + # Handle program arguments + ap = argparse.ArgumentParser( + prog="usdnc-to-usd", description="Convert USDNC files to USD" + ) + ap.add_argument("input", help="Input file", type=Path) + ap.add_argument( + "--output", + "-o", + help="Output file. Defaults to the input file with a new extension.", + type=Path, + default=None, + ) + ap.add_argument( + "--format", + "-f", + help="Output format. Defaults to usda.", + type=str, + default="usda", + choices=["usda", "usdc"], + ) + args = ap.parse_args() + + # Read the input file + print(f"Opening stage from: {args.input}") + stage = Usd.Stage.Open(args.input.as_posix()) + + # Determine the output file + if not args.output: + args.output = args.input.with_suffix(f".{args.format}") + + # Write the output file + print(f"Writing stage to: {args.output}") + stage.Export(args.output.as_posix()) + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/configs/ssh/config b/configs/ssh/config index 85669c3..ed2ef05 100644 --- a/configs/ssh/config +++ b/configs/ssh/config @@ -74,6 +74,8 @@ Host sdf.org *.sdf.org # Guru Host *.gurustudio.com User "guru-domain\\epratten" +Host td-prod td-prod2 td-prod3 td-prod4 + User guru # Personal Infra Host oci-arm diff --git a/install.conf.yaml b/install.conf.yaml index 8cd76c1..21d61e1 100644 --- a/install.conf.yaml +++ b/install.conf.yaml @@ -55,6 +55,9 @@ ~/bin/run-logid: path: configs/scripts/run-logid mode: 755 + ~/bin/houdini-tool: configs/scripts/houdini-tool.py + ~/bin/hython-latest: configs/scripts/hython-latest + ~/bin/usdnc-to-usd: configs/scripts/usdnc-to-usd.py # Systemd Services # ~/.config/systemd/user/logid.service: configs/systemd/user/logid.service @@ -71,6 +74,7 @@ ~/.config/termux/termux.properties: configs/termux/termux.properties ~/.config/user-tmpfiles.d/discord-rpc.conf: configs/user-tmpfiles.d/discord-rpc.conf ~/.config/logid/logid.cfg: configs/logid/logid.cfg + ~/houdini19.5/scripts: configs/houdini19.5/scripts - shell: # Make sure we have our git modules @@ -78,9 +82,7 @@ # Install SSH config - [sh ./helpers/install-ssh-config.sh, Installing SSH config] # Ensure that all downloaded scripts are executable - - [chmod +x configs/scripts/catto, Making catto executable] - - [chmod +x configs/scripts/aspath, Making aspath executable] - - [chmod +x configs/scripts/fetch-steamdeck-screenshots, Making fetch-steamdeck-screenshots executable] + - [chmod +x configs/scripts/*, Making bin scripts executable] - [chmod +x configs/nautilus/scripts/*, Making nautilus scripts executable] # Configure GNOME - [sh ./helpers/configure-gnome.sh, Configuring GNOME] diff --git a/python_modules/ewpipe/common/dirs.py b/python_modules/ewpipe/common/dirs.py new file mode 100644 index 0000000..5a4546b --- /dev/null +++ b/python_modules/ewpipe/common/dirs.py @@ -0,0 +1,13 @@ +from pathlib import Path + +DCC_DATA_BASE_DIR = Path.home() / "Videos" / "DCC" +"""The base directory for storing data across DCCs""" + +HOUDINI_BASE_DIR = DCC_DATA_BASE_DIR / "Houdini" +"""The base directory for storing Houdini data""" + +HOUDINI_PROJECTS_DIR = HOUDINI_BASE_DIR / "Projects" +"""The base directory for storing Houdini projects""" + +BLENDER_BASE_DIR = DCC_DATA_BASE_DIR / "Blender" +"""The base directory for storing Blender data""" diff --git a/python_modules/ewpipe/common/env.py b/python_modules/ewpipe/common/env.py new file mode 100644 index 0000000..0d1368a --- /dev/null +++ b/python_modules/ewpipe/common/env.py @@ -0,0 +1,31 @@ +import os +from typing import Dict + + +def diff_environments(env_1: Dict[str, str], env_2: Dict[str, str]) -> Dict[str, str]: + """Diff two environments. + + Args: + env_1 (Dict[str,str]): First environment + env_2 (Dict[str,str]): Second environment + + Returns: + Dict[str,str]: Difference between the two environments + """ + return { + key: value + for key, value in env_1.items() + if key not in env_2 or env_2[key] != value + } + + +def diff_from_current_env(new_env: Dict[str, str]) -> Dict[str, str]: + """Diff the current environment from the given environment. + + Args: + new_env (Dict[str, str]): New environment + + Returns: + Dict[str, str]: Difference between the current environment and the given environment + """ + return diff_environments(os.environ, new_env) # type: ignore diff --git a/python_modules/ewpipe/common/logging.py b/python_modules/ewpipe/common/logging.py new file mode 100644 index 0000000..b45ad94 --- /dev/null +++ b/python_modules/ewpipe/common/logging.py @@ -0,0 +1,8 @@ +import logging + + +def configure_logging(verbose: bool = False): + logging.basicConfig( + level=logging.DEBUG if verbose else logging.INFO, + format="%(levelname)s:\t%(message)s", + ) diff --git a/python_modules/ewpipe/houdini/editions.py b/python_modules/ewpipe/houdini/editions.py new file mode 100644 index 0000000..af487c8 --- /dev/null +++ b/python_modules/ewpipe/houdini/editions.py @@ -0,0 +1,36 @@ +from typing import List + +HOU_EDITIONS = ["core", "fx", "indie", "apprentice"] +"""All possible Houdini editions.""" + + +def get_binary_name_for_edition(edition: str) -> str: + """Get the appropriate binary name for the given Houdini edition. + + Args: + edition (str): Hooudini edition + + Returns: + str: Binary name + """ + + if edition in ["core", "fx"]: + return f"houdini{edition}" + else: + return "houdini" + + +def get_houdini_edition_args(edition: str) -> List[str]: + """Get the appropriate arguments to launch a given Houdini edition. + + Args: + edition (str): Houdini edition + + Returns: + List[str]: Arguments + """ + + if edition in ["indie", "apprentice"]: + return [f"-{edition}"] + else: + return [] diff --git a/python_modules/ewpipe/houdini/installations.py b/python_modules/ewpipe/houdini/installations.py new file mode 100644 index 0000000..a6c009a --- /dev/null +++ b/python_modules/ewpipe/houdini/installations.py @@ -0,0 +1,129 @@ +import logging +import platform +import argparse +import sys +from pathlib import Path +from typing import Optional + +logger = logging.getLogger(__name__) + + +def get_default_houdini_installation_base_path() -> Path: + """Get the default Houdini installation base path. + + Returns: + Path: Default Houdini installation base path + """ + if platform.system() == "Linux": + return Path("/opt") + elif platform.system() == "Windows": + return Path("C:/Program Files/Side Effects Software") + else: + raise RuntimeError(f"Unsupported platform: {platform.system()}") + + +def find_latest_houdini_installation(base_path: Path) -> Optional[Path]: + """Find the latest Houdini installation in the given base path. + + Args: + base_path (Path): Base path to look for Houdini installations in. + + Returns: + Optional[Path]: Houdini installation path if found + """ + logger.debug(f"Looking for the latest Houdini installation in: {base_path}") + + # Look for possible houdini installations + if platform.system() == "Linux": + possible_installations = sorted(base_path.glob("hfs*")) + elif platform.system() == "Windows": + possible_installations = sorted(base_path.glob("Houdini *")) + else: + raise RuntimeError(f"Unsupported platform: {platform.system()}") + logger.debug( + f"Search found the following Houdini installations: {[str(i) for i in possible_installations]}" + ) + + # Remove `Houdini Server` if it exists + possible_installations = [ + installation + for installation in possible_installations + if "Server" not in installation.name + ] + + # If there are no installations, return None + if not possible_installations: + return None + + # Otherwise, return the latest installation + latest_installation = possible_installations[-1] + logger.debug(f"Latest Houdini installation: {latest_installation}") + return latest_installation + + +def get_houdini_installation_path( + version: Optional[str] = None, + base_path: Optional[Path] = None, + not_exists_ok: bool = False, +) -> Optional[Path]: + """Get the path to the Houdini installation for the given version. + + Args: + version (Optional[str], optional): Houdini version to target. Defaults to None. + not_exists_ok (bool, optional): If true, allows bad paths to be returned. Defaults to False. + + Raises: + RuntimeError: Thrown if the platform is not supported. + + Returns: + Optional[Path]: Path to the Houdini installation if found + """ + + logger.debug(f"Finding Houdini installation for version: {version}") + + # Get the default installation base path + if not base_path: + base_path = get_default_houdini_installation_base_path() + logger.debug(f"Searching for Houdini installations in: {base_path}") + + # If we don't have a version, find the latest installation + if not version: + logger.debug("No version specified, finding latest installation") + return find_latest_houdini_installation(base_path) + + # Otherwise, find the installation for the given version + if platform.system() == "Linux": + installation_path = base_path / f"hfs{version}" + elif platform.system() == "Windows": + installation_path = base_path / f"Houdini {version}" + else: + raise RuntimeError(f"Unsupported platform: {platform.system()}") + + # If the installation path does not exist, return None + if (not installation_path.exists()) and not not_exists_ok: + logger.debug(f"Installation path does not exist: {installation_path}") + return None + + # Otherwise, return the installation path + logger.debug(f"Found installation path: {installation_path}") + return installation_path + + +if __name__ == "__main__": + ap = argparse.ArgumentParser() + ap.add_argument("--version", "-v", help="Houdini version", type=str) + ap.add_argument("--base-path", "-b", help="Houdini base path", type=str) + ap.add_argument("--not-exists-ok", help="Allow bad paths", action="store_true") + args = ap.parse_args() + + result = get_houdini_installation_path( + version=args.version, + base_path=Path(args.base_path) if args.base_path else None, + not_exists_ok=args.not_exists_ok, + ) + if not result: + print("Could not find Houdini", file=sys.stderr) + sys.exit(1) + + print(result) + sys.exit(0)