added mechanism to define custom paths to ffmpeg and oiio tools and more detailed validation of them

This commit is contained in:
Jakub Trllo 2022-10-13 19:14:48 +02:00
parent 22222942e5
commit 840792a82c

View file

@ -1,10 +1,33 @@
import os
import logging
import platform
import subprocess
log = logging.getLogger("Vendor utils")
class CachedToolPaths:
"""Cache already used and discovered tools and their executables.
Discovering path can take some time and can trigger subprocesses so it's
better to cache the paths on first get.
"""
_cached_paths = {}
@classmethod
def is_tool_cached(cls, tool):
return tool in cls._cached_paths
@classmethod
def get_executable_path(cls, tool):
return cls._cached_paths.get(tool)
@classmethod
def cache_executable_path(cls, tool, path):
cls._cached_paths[tool] = path
def is_file_executable(filepath):
"""Filepath lead to executable file.
@ -98,6 +121,7 @@ def get_vendor_bin_path(bin_app):
Returns:
str: Path to vendorized binaries folder.
"""
return os.path.join(
os.environ["OPENPYPE_ROOT"],
"vendor",
@ -107,6 +131,112 @@ def get_vendor_bin_path(bin_app):
)
def find_tool_in_custom_paths(paths, tool, validation_func=None):
"""Find a tool executable in custom paths.
Args:
paths (Iterable[str]): Iterable of paths where to look for tool.
tool (str): Name of tool (binary file) to find in passed paths.
validation_func (Function): Custom validation function of path.
Function must expect one argument which is path to executable.
If not passed only 'find_executable' is used to be able identify
if path is valid.
Reuturns:
Union[str, None]: Path to validated executable or None if was not
found.
"""
for path in paths:
# Skip empty strings
if not path:
continue
# Handle cases when path is just an executable
# - it allows to use executable from PATH
# - basename must match 'tool' value (without extension)
extless_path, ext = os.path.splitext(path)
if extless_path == tool:
executable_path = find_executable(tool)
if executable_path and (
validation_func is None
or validation_func(executable_path)
):
return executable_path
continue
# Normalize path because it should be a path and check if exists
normalized = os.path.normpath(path)
if not os.path.exists(normalized):
continue
# Note: Path can be both file and directory
# If path is a file validate it
if os.path.isfile(normalized):
basename, ext = os.path.splitext(os.path.basename(path))
# Check if the filename has actually the sane bane as 'tool'
if basename == tool:
executable_path = find_executable(normalized)
if executable_path and (
validation_func is None
or validation_func(executable_path)
):
return executable_path
# Check if path is a directory and look for tool inside the dir
if os.path.isdir(normalized):
executable_path = find_executable(os.path.join(normalized, tool))
if executable_path and (
validation_func is None
or validation_func(executable_path)
):
return executable_path
return None
def _oiio_executable_validation(filepath):
"""Validate oiio tool executable if can be executed.
Validation has 2 steps. First is using 'find_executable' to fill possible
missing extension or fill directory then launch executable and validate
that it can be executed. For that is used '--help' argument which is fast
and does not need any other inputs.
Any possible crash of missing libraries or invalid build should be catched.
Main reason is to validate if executable can be executed on OS just running
which can be issue ob linux machines.
Note:
It does not validate if the executable is really a oiio tool which
should be used.
Args:
filepath (str): Path to executable.
Returns:
bool: Filepath is valid executable.
"""
filepath = find_executable(filepath)
if not filepath:
return False
try:
proc = subprocess.Popen(
[filepath, "--help"],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
proc.wait()
return proc.returncode == 0
except Exception:
pass
return False
def get_oiio_tools_path(tool="oiiotool"):
"""Path to vendorized OpenImageIO tool executables.
@ -117,10 +247,67 @@ def get_oiio_tools_path(tool="oiiotool"):
Default is "oiiotool".
"""
oiio_dir = get_vendor_bin_path("oiio")
if platform.system().lower() == "linux":
oiio_dir = os.path.join(oiio_dir, "bin")
return find_executable(os.path.join(oiio_dir, tool))
if CachedToolPaths.is_tool_cached(tool):
return CachedToolPaths.get_executable_path(tool)
custom_paths_str = os.environ.get("OPENPYPE_CUSTOM_OIIO_PATHS") or ""
tool_executable_path = find_tool_in_custom_paths(
custom_paths_str.split(os.pathsep),
tool,
_oiio_executable_validation
)
if not tool_executable_path:
oiio_dir = get_vendor_bin_path("oiio")
if platform.system().lower() == "linux":
oiio_dir = os.path.join(oiio_dir, "bin")
default_path = os.path.join(oiio_dir, tool)
if _oiio_executable_validation(default_path):
tool_executable_path = default_path
CachedToolPaths.cache_executable_path(tool, tool_executable_path)
return tool_executable_path
def _ffmpeg_executable_validation(filepath):
"""Validate ffmpeg tool executable if can be executed.
Validation has 2 steps. First is using 'find_executable' to fill possible
missing extension or fill directory then launch executable and validate
that it can be executed. For that is used '-version' argument which is fast
and does not need any other inputs.
Any possible crash of missing libraries or invalid build should be catched.
Main reason is to validate if executable can be executed on OS just running
which can be issue ob linux machines.
Note:
It does not validate if the executable is really a ffmpeg tool.
Args:
filepath (str): Path to executable.
Returns:
bool: Filepath is valid executable.
"""
filepath = find_executable(filepath)
if not filepath:
return False
try:
proc = subprocess.Popen(
[filepath, "-version"],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
proc.wait()
return proc.returncode == 0
except Exception:
pass
return False
def get_ffmpeg_tool_path(tool="ffmpeg"):
@ -133,10 +320,27 @@ def get_ffmpeg_tool_path(tool="ffmpeg"):
Returns:
str: Full path to ffmpeg executable.
"""
ffmpeg_dir = get_vendor_bin_path("ffmpeg")
if platform.system().lower() == "windows":
ffmpeg_dir = os.path.join(ffmpeg_dir, "bin")
return find_executable(os.path.join(ffmpeg_dir, tool))
if CachedToolPaths.is_tool_cached(tool):
return CachedToolPaths.get_executable_path(tool)
custom_paths_str = os.environ.get("OPENPYPE_CUSTOM_FFMPEG_PATHS") or ""
tool_executable_path = find_tool_in_custom_paths(
custom_paths_str.split(os.pathsep),
tool,
_ffmpeg_executable_validation
)
if not tool_executable_path:
ffmpeg_dir = get_vendor_bin_path("ffmpeg")
if platform.system().lower() == "windows":
ffmpeg_dir = os.path.join(ffmpeg_dir, "bin")
tool_path = find_executable(os.path.join(ffmpeg_dir, tool))
if tool_path and _ffmpeg_executable_validation(tool_path):
tool_executable_path = tool_path
CachedToolPaths.cache_executable_path(tool, tool_executable_path)
return tool_executable_path
def is_oiio_supported():