From 9265241202d7a63c8fcdabb1a275758b0ae7293f Mon Sep 17 00:00:00 2001 From: Michael Oliver Date: Tue, 29 Sep 2026 10:59:46 -0700 Subject: [PATCH 1/5] fix: resolve Windows CLI dependency paths and implement OIIO image writing (fixes #58, fixes #59) - Fixes #58: Configure OpenUTVDeps bin, python, and PySide6 directories in PATH and set OPENUTV_DEPS_ROOT, PYTHONHOME, QT_PLUGIN_PATH, and QT_QPA_PLATFORM_PLUGIN_PATH in install.ps1. Auto-discover dependencies and configure environment in openutv-diagnostics.cmd and openutv-check-updates.cmd. Remove obsolete legacy qt.conf that blocked Qt 6 plugin resolution. Add --help and --silent support to openutv-diagnostics.py. --- packages/rv/CMakeLists.txt | 4 +- scripts/install.ps1 | 141 ++++++++---- src/bin/apps/rv/UTVLauncherWin.cpp | 20 ++ src/bin/nsapps/UTV/openutv-check-updates.cmd | 35 ++- src/bin/nsapps/UTV/openutv-diagnostics.cmd | 35 ++- src/bin/nsapps/UTV/openutv-diagnostics.py | 21 +- src/lib/image/IOoiio/IOoiio.cpp | 228 +++++++++++++++++-- src/lib/image/TwkFB/IO.cpp | 15 +- 8 files changed, 420 insertions(+), 79 deletions(-) diff --git a/packages/rv/CMakeLists.txt b/packages/rv/CMakeLists.txt index bdb16f83..57fdeeca 100644 --- a/packages/rv/CMakeLists.txt +++ b/packages/rv/CMakeLists.txt @@ -19,7 +19,9 @@ CONFIGURE_FILE(rviorc.mu ${RV_STAGE_SCRIPTS_DIR}/utv/rviorc.mu COPYONLY) CONFIGURE_FILE(rvrc.py ${RV_STAGE_SCRIPTS_DIR}/utv/rvrc.py COPYONLY) CONFIGURE_FILE(rvrc.mu ${RV_STAGE_SCRIPTS_DIR}/utv/utvrc.mu COPYONLY) CONFIGURE_FILE(rvrc.py ${RV_STAGE_SCRIPTS_DIR}/utv/utvrc.py COPYONLY) -IF(NOT RV_USE_SYSTEM_DEPS) +IF(NOT RV_USE_SYSTEM_DEPS + AND NOT RV_TARGET_WINDOWS +) CONFIGURE_FILE(qt.conf ${RV_STAGE_BIN_DIR}/qt.conf COPYONLY) ENDIF() diff --git a/scripts/install.ps1 b/scripts/install.ps1 index 660efabe..8ff77c17 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -112,51 +112,52 @@ Write-Host "Target Directory: $InstallDir" -ForegroundColor White Write-Host "Administrative Privileges: $isAdmin`n" -ForegroundColor White # Step 1: Check and Install OpenUTVDeps -if (-not $SkipDeps) { - Write-Host "--- Checking OpenUTV Dependencies (v$DepsVersion) ---" -ForegroundColor Cyan - $depsFound = $false - $detectedDepsPath = "" - - if ($env:OPENUTV_DEPS_ROOT -and (Test-Path "$env:OPENUTV_DEPS_ROOT\bin\OpenImageIO.dll")) { - $depsFound = $true - $detectedDepsPath = $env:OPENUTV_DEPS_ROOT - } elseif (Test-Path "C:\Program Files\OpenUTVDeps $DepsVersion\bin\OpenImageIO.dll") { - $depsFound = $true - $detectedDepsPath = "C:\Program Files\OpenUTVDeps $DepsVersion" - } elseif (Test-Path "C:\Program Files\OpenUTVDeps*\bin\OpenImageIO.dll") { - $found = Get-Item "C:\Program Files\OpenUTVDeps*\bin\OpenImageIO.dll" | Select-Object -First 1 +Write-Host "--- Checking OpenUTV Dependencies (v$DepsVersion) ---" -ForegroundColor Cyan +$depsFound = $false +$detectedDepsPath = "" + +if (Test-Path "C:\Program Files\OpenUTVDeps $DepsVersion\bin\OpenImageIO.dll") { + $depsFound = $true + $detectedDepsPath = "C:\Program Files\OpenUTVDeps $DepsVersion" +} elseif ($env:OPENUTV_DEPS_ROOT -and (Test-Path "$env:OPENUTV_DEPS_ROOT\bin\OpenImageIO.dll")) { + $depsFound = $true + $detectedDepsPath = $env:OPENUTV_DEPS_ROOT +} elseif (Test-Path "C:\Program Files\OpenUTVDeps*\bin\OpenImageIO.dll") { + $found = Get-Item "C:\Program Files\OpenUTVDeps*\bin\OpenImageIO.dll" | Select-Object -First 1 + $depsFound = $true + $detectedDepsPath = $found.Directory.Parent.FullName +} else { + $uninst = Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*" -ErrorAction SilentlyContinue | + Where-Object { $_.DisplayName -like "OpenUTVDeps $DepsVersion*" -or $_.DisplayName -like "OpenUTV Dependencies $DepsVersion*" } | + Select-Object -First 1 + if ($uninst -and $uninst.InstallLocation -and (Test-Path "$($uninst.InstallLocation)\bin\OpenImageIO.dll")) { $depsFound = $true - $detectedDepsPath = $found.Directory.Parent.FullName - } else { - $uninst = Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*" -ErrorAction SilentlyContinue | - Where-Object { $_.DisplayName -like "OpenUTVDeps $DepsVersion*" -or $_.DisplayName -like "OpenUTV Dependencies $DepsVersion*" } | - Select-Object -First 1 - if ($uninst -and $uninst.InstallLocation -and (Test-Path "$($uninst.InstallLocation)\bin\OpenImageIO.dll")) { - $depsFound = $true - $detectedDepsPath = $uninst.InstallLocation.TrimEnd('\') - } + $detectedDepsPath = $uninst.InstallLocation.TrimEnd('\') } +} - if ($depsFound) { - Write-Host "OpenUTV dependencies detected at: $detectedDepsPath" -ForegroundColor Green - } else { - Write-Host "OpenUTV dependencies not found. Downloading OpenUTVDeps MSI installer..." -ForegroundColor Yellow - $msiUrl = "https://github.com/$RepoOwner/$DepsRepoName/releases/download/v$DepsVersion/OpenUTVDeps-$DepsVersion-win64.msi" - $tempMsi = Join-Path $env:TEMP "OpenUTVDeps-$DepsVersion-win64.msi" +if ($depsFound) { + Write-Host "OpenUTV dependencies detected at: $detectedDepsPath" -ForegroundColor Green +} elseif (-not $SkipDeps) { + Write-Host "OpenUTV dependencies not found. Downloading OpenUTVDeps MSI installer..." -ForegroundColor Yellow + $msiUrl = "https://github.com/$RepoOwner/$DepsRepoName/releases/download/v$DepsVersion/OpenUTVDeps-$DepsVersion-win64.msi" + $tempMsi = Join-Path $env:TEMP "OpenUTVDeps-$DepsVersion-win64.msi" - Write-Host "Downloading $msiUrl..." -ForegroundColor White - Invoke-WebRequest -Uri $msiUrl -OutFile $tempMsi -UseBasicParsing + Write-Host "Downloading $msiUrl..." -ForegroundColor White + Invoke-WebRequest -Uri $msiUrl -OutFile $tempMsi -UseBasicParsing - Write-Host "Installing OpenUTVDeps (silent MSI install)..." -ForegroundColor Yellow - $msiProc = Start-Process msiexec.exe -ArgumentList "/i `"$tempMsi`" /qn /norestart MSIFASTINSTALL=7" -Wait -PassThru - Remove-Item -Force $tempMsi -ErrorAction SilentlyContinue + Write-Host "Installing OpenUTVDeps (silent MSI install)..." -ForegroundColor Yellow + $msiProc = Start-Process msiexec.exe -ArgumentList "/i `"$tempMsi`" /qn /norestart MSIFASTINSTALL=7" -Wait -PassThru + Remove-Item -Force $tempMsi -ErrorAction SilentlyContinue - if ($msiProc.ExitCode -ne 0 -and $msiProc.ExitCode -ne 3010) { - Write-Error "Failed to install OpenUTVDeps MSI. Exit code: $($msiProc.ExitCode)" - return - } - Write-Host "OpenUTVDeps installed successfully!" -ForegroundColor Green + if ($msiProc.ExitCode -ne 0 -and $msiProc.ExitCode -ne 3010) { + Write-Error "Failed to install OpenUTVDeps MSI. Exit code: $($msiProc.ExitCode)" + return } + Write-Host "OpenUTVDeps installed successfully!" -ForegroundColor Green + $detectedDepsPath = "C:\Program Files\OpenUTVDeps $DepsVersion" +} else { + Write-Host "Skipping OpenUTVDeps download/install (-SkipDeps)." -ForegroundColor Gray } # Step 2: Resolve UTV Release Asset @@ -208,18 +209,70 @@ if (-not (Test-Path $utvExe)) { return } -# Step 4: Configure PATH +# Step 4: Configure PATH and Environment Write-Host "`n--- Configuring Environment ---" -ForegroundColor Cyan $binDir = Join-Path $InstallDir "bin" $pathScope = if ($isAdmin) { "Machine" } else { "User" } + +# Remove orphaned legacy qt.conf if present without plugins\Qt (prevents blocking PySide6 Qt plugin loading) +$legacyQtConf = Join-Path $binDir "qt.conf" +$qtPluginDir = Join-Path $InstallDir "plugins\Qt" +if ((Test-Path $legacyQtConf) -and -not (Test-Path $qtPluginDir)) { + Remove-Item -Force $legacyQtConf -ErrorAction SilentlyContinue + Write-Host "Removed legacy qt.conf." -ForegroundColor Gray +} + +# Collect paths to add to PATH +$pathsToAdd = @($binDir) + +if ($detectedDepsPath -and (Test-Path $detectedDepsPath)) { + $depsBin = Join-Path $detectedDepsPath "bin" + $depsPython = Join-Path $detectedDepsPath "tools\python3" + $depsPySide = Join-Path $detectedDepsPath "tools\python3\Lib\site-packages\PySide6" + $depsPlugins = Join-Path $depsPySide "plugins" + $depsPlatforms = Join-Path $depsPlugins "platforms" + + if (Test-Path $depsBin) { $pathsToAdd += $depsBin } + if (Test-Path $depsPython) { $pathsToAdd += $depsPython } + if (Test-Path $depsPySide) { $pathsToAdd += $depsPySide } + + # Set persistent OpenUTVDeps environment variables + [Environment]::SetEnvironmentVariable("OPENUTV_DEPS_ROOT", $detectedDepsPath, $pathScope) + $env:OPENUTV_DEPS_ROOT = $detectedDepsPath + + if (Test-Path $depsPython) { + [Environment]::SetEnvironmentVariable("PYTHONHOME", $depsPython, $pathScope) + $env:PYTHONHOME = $depsPython + } + if (Test-Path $depsPlugins) { + [Environment]::SetEnvironmentVariable("QT_PLUGIN_PATH", $depsPlugins, $pathScope) + $env:QT_PLUGIN_PATH = $depsPlugins + } + if (Test-Path $depsPlatforms) { + [Environment]::SetEnvironmentVariable("QT_QPA_PLATFORM_PLUGIN_PATH", $depsPlatforms, $pathScope) + $env:QT_QPA_PLATFORM_PLUGIN_PATH = $depsPlatforms + } +} + $currentEnvPath = [Environment]::GetEnvironmentVariable("Path", $pathScope) -if ($currentEnvPath -notlike "*$binDir*") { - $newEnvPath = "$binDir;$currentEnvPath" +$currentList = ($currentEnvPath -split ";") | Where-Object { $_ -ne "" } +$newEntries = @() +foreach ($p in $pathsToAdd) { + if ($currentList -notcontains $p) { + $newEntries += $p + } +} + +if ($newEntries.Count -gt 0) { + $newEnvPath = ($newEntries + $currentList) -join ";" [Environment]::SetEnvironmentVariable("Path", $newEnvPath, $pathScope) - $env:Path = "$binDir;$env:Path" - Write-Host "Added $binDir to $pathScope PATH." -ForegroundColor Green + $env:Path = ($newEntries + ($env:Path -split ";")) -join ";" + Write-Host "Added $($newEntries.Count) OpenUTV path(s) to $pathScope PATH." -ForegroundColor Green + foreach ($entry in $newEntries) { + Write-Host " + $entry" -ForegroundColor Gray + } } else { - Write-Host "$binDir is already present in $pathScope PATH." -ForegroundColor Gray + Write-Host "OpenUTV binaries and dependencies already present in $pathScope PATH." -ForegroundColor Gray } # Step 5: Create Start Menu and Desktop Shortcuts diff --git a/src/bin/apps/rv/UTVLauncherWin.cpp b/src/bin/apps/rv/UTVLauncherWin.cpp index 84efc8a5..0210a819 100644 --- a/src/bin/apps/rv/UTVLauncherWin.cpp +++ b/src/bin/apps/rv/UTVLauncherWin.cpp @@ -791,6 +791,26 @@ int RunLauncher() } } + // Configure QT_QPA_PLATFORM_PLUGIN_PATH if not explicitly specified + if (GetEnvironmentVariableW(L"QT_QPA_PLATFORM_PLUGIN_PATH", NULL, 0) == 0) + { + if (!depsPySide.empty()) + { + std::wstring platformsDir = depsPySide + L"\\plugins\\platforms"; + if (DirExists(platformsDir)) + { + SetEnvironmentVariableW(L"QT_QPA_PLATFORM_PLUGIN_PATH", platformsDir.c_str()); + } + } + } + + // Remove legacy orphaned qt.conf if plugins/Qt does not exist (prevents blocking PySide6 Qt plugin loading) + std::wstring legacyQtConf = appDir + L"\\qt.conf"; + if (FileExists(legacyQtConf) && !DirExists(appDir + L"\\plugins\\Qt")) + { + DeleteFileW(legacyQtConf.c_str()); + } + // Configure QtWebEngine and QML paths if PySide6 Qt is used if (!depsPySide.empty()) { diff --git a/src/bin/nsapps/UTV/openutv-check-updates.cmd b/src/bin/nsapps/UTV/openutv-check-updates.cmd index d49a57df..eacac02c 100644 --- a/src/bin/nsapps/UTV/openutv-check-updates.cmd +++ b/src/bin/nsapps/UTV/openutv-check-updates.cmd @@ -1,8 +1,41 @@ @echo off -setlocal +setlocal enabledelayedexpansion set "DIR=%~dp0" + +:: 1. Discover OpenUTVDeps Root +if not defined OPENUTV_DEPS_ROOT ( + for /f "tokens=2*" %%a in ('reg query "HKCU\Software\OpenUTV" /v "DepsPath" 2^>nul') do set "OPENUTV_DEPS_ROOT=%%b" +) +if not defined OPENUTV_DEPS_ROOT ( + for /f "tokens=2*" %%a in ('reg query "HKLM\Software\OpenUTV" /v "DepsPath" 2^>nul') do set "OPENUTV_DEPS_ROOT=%%b" +) +if not defined OPENUTV_DEPS_ROOT ( + for /d %%d in ("%ProgramFiles%\OpenUTVDeps*") do if exist "%%d\bin\OpenImageIO.dll" set "OPENUTV_DEPS_ROOT=%%d" +) +if not defined OPENUTV_DEPS_ROOT ( + for /d %%d in ("%LOCALAPPDATA%\Programs\OpenUTVDeps*") do if exist "%%d\bin\OpenImageIO.dll" set "OPENUTV_DEPS_ROOT=%%d" +) + +:: 2. Configure Environment if Dependencies Found +if defined OPENUTV_DEPS_ROOT ( + set "DEPS_BIN=%OPENUTV_DEPS_ROOT%\bin" + set "DEPS_PY=%OPENUTV_DEPS_ROOT%\tools\python3" + set "DEPS_PYSIDE=%OPENUTV_DEPS_ROOT%\tools\python3\Lib\site-packages\PySide6" + set "PATH=!DEPS_BIN!;!DEPS_PY!;!DEPS_PYSIDE!;%DIR%;!PATH!" + if not defined PYTHONHOME set "PYTHONHOME=!DEPS_PY!" + if not defined QT_PLUGIN_PATH if exist "!DEPS_PYSIDE!\plugins" set "QT_PLUGIN_PATH=!DEPS_PYSIDE!\plugins" + if not defined QT_QPA_PLATFORM_PLUGIN_PATH if exist "!DEPS_PYSIDE!\plugins\platforms" set "QT_QPA_PLATFORM_PLUGIN_PATH=!DEPS_PYSIDE!\plugins\platforms" +) + +:: 3. Remove orphaned legacy qt.conf if present without plugins\Qt +if exist "%DIR%qt.conf" if not exist "%DIR%..\plugins\Qt" del /f /q "%DIR%qt.conf" >nul 2>&1 + +:: 4. Execute Update Checker if exist "%DIR%py-interp.exe" ( "%DIR%py-interp.exe" "%DIR%openutv-check-updates.py" %* +) else if defined DEPS_PY if exist "!DEPS_PY!\python.exe" ( + "!DEPS_PY!\python.exe" "%DIR%openutv-check-updates.py" %* ) else ( python "%DIR%openutv-check-updates.py" %* ) +exit /b %ERRORLEVEL% diff --git a/src/bin/nsapps/UTV/openutv-diagnostics.cmd b/src/bin/nsapps/UTV/openutv-diagnostics.cmd index 74bd88c5..091ad3f9 100644 --- a/src/bin/nsapps/UTV/openutv-diagnostics.cmd +++ b/src/bin/nsapps/UTV/openutv-diagnostics.cmd @@ -1,8 +1,41 @@ @echo off -setlocal +setlocal enabledelayedexpansion set "DIR=%~dp0" + +:: 1. Discover OpenUTVDeps Root +if not defined OPENUTV_DEPS_ROOT ( + for /f "tokens=2*" %%a in ('reg query "HKCU\Software\OpenUTV" /v "DepsPath" 2^>nul') do set "OPENUTV_DEPS_ROOT=%%b" +) +if not defined OPENUTV_DEPS_ROOT ( + for /f "tokens=2*" %%a in ('reg query "HKLM\Software\OpenUTV" /v "DepsPath" 2^>nul') do set "OPENUTV_DEPS_ROOT=%%b" +) +if not defined OPENUTV_DEPS_ROOT ( + for /d %%d in ("%ProgramFiles%\OpenUTVDeps*") do if exist "%%d\bin\OpenImageIO.dll" set "OPENUTV_DEPS_ROOT=%%d" +) +if not defined OPENUTV_DEPS_ROOT ( + for /d %%d in ("%LOCALAPPDATA%\Programs\OpenUTVDeps*") do if exist "%%d\bin\OpenImageIO.dll" set "OPENUTV_DEPS_ROOT=%%d" +) + +:: 2. Configure Environment if Dependencies Found +if defined OPENUTV_DEPS_ROOT ( + set "DEPS_BIN=%OPENUTV_DEPS_ROOT%\bin" + set "DEPS_PY=%OPENUTV_DEPS_ROOT%\tools\python3" + set "DEPS_PYSIDE=%OPENUTV_DEPS_ROOT%\tools\python3\Lib\site-packages\PySide6" + set "PATH=!DEPS_BIN!;!DEPS_PY!;!DEPS_PYSIDE!;%DIR%;!PATH!" + if not defined PYTHONHOME set "PYTHONHOME=!DEPS_PY!" + if not defined QT_PLUGIN_PATH if exist "!DEPS_PYSIDE!\plugins" set "QT_PLUGIN_PATH=!DEPS_PYSIDE!\plugins" + if not defined QT_QPA_PLATFORM_PLUGIN_PATH if exist "!DEPS_PYSIDE!\plugins\platforms" set "QT_QPA_PLATFORM_PLUGIN_PATH=!DEPS_PYSIDE!\plugins\platforms" +) + +:: 3. Remove orphaned legacy qt.conf if present without plugins\Qt +if exist "%DIR%qt.conf" if not exist "%DIR%..\plugins\Qt" del /f /q "%DIR%qt.conf" >nul 2>&1 + +:: 4. Execute Diagnostics if exist "%DIR%py-interp.exe" ( "%DIR%py-interp.exe" "%DIR%openutv-diagnostics.py" %* +) else if defined DEPS_PY if exist "!DEPS_PY!\python.exe" ( + "!DEPS_PY!\python.exe" "%DIR%openutv-diagnostics.py" %* ) else ( python "%DIR%openutv-diagnostics.py" %* ) +exit /b %ERRORLEVEL% diff --git a/src/bin/nsapps/UTV/openutv-diagnostics.py b/src/bin/nsapps/UTV/openutv-diagnostics.py index 97dc3ddd..edfeab33 100755 --- a/src/bin/nsapps/UTV/openutv-diagnostics.py +++ b/src/bin/nsapps/UTV/openutv-diagnostics.py @@ -425,7 +425,18 @@ def notify_user(zip_path): def main(): + if "--help" in sys.argv or "-h" in sys.argv: + print("Usage: openutv-diagnostics [OPTIONS]") + print("Collects system info, logs, crash reports, and configs into a zip archive.") + print() + print("Options:") + print(" --no-browser Do not open the GitHub bug report page in a browser") + print(" --silent Do not show GUI dialogs or open file manager (for headless/scripts)") + print(" --help, -h Show this help message") + sys.exit(0) + no_browser = "--no-browser" in sys.argv + silent = "--silent" in sys.argv or "--quiet" in sys.argv # 1. Setup temp collection directory temp_base = tempfile.mkdtemp(prefix="openutv_diag_") @@ -465,14 +476,18 @@ def main(): pass # 4. Reveal zip file in Finder / Explorer / Nautilus - reveal_file(zip_path) + if not silent: + reveal_file(zip_path) # 5. Open browser if requested - if not no_browser: + if not no_browser and not silent: webbrowser.open(GITHUB_BUG_URL) # 6. Show confirmation dialog - notify_user(zip_path) + if not silent: + notify_user(zip_path) + else: + print(f"Diagnostics package created successfully: {zip_path}") sys.exit(0) diff --git a/src/lib/image/IOoiio/IOoiio.cpp b/src/lib/image/IOoiio/IOoiio.cpp index 26792eb3..a9cba900 100644 --- a/src/lib/image/IOoiio/IOoiio.cpp +++ b/src/lib/image/IOoiio/IOoiio.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -32,45 +33,53 @@ namespace TwkFB unsigned int r = ImageRead; unsigned int w = ImageWrite; unsigned int rw = r | w; + + // Dedicated native writer plugins handle the following formats with custom streaming pipelines: + // IOpng ("m9"), IOdpx ("m5"), IOcin ("m4"), IOjpeg ("m2"), IOtarga ("m6"), IOrla ("m7"), IOrgbe ("m8"). + // These MUST be registered as Read-only in IOoiio so write requests route directly to their native writers. + addType("png", "Portable Network Graphics Image", r, codecs); + addType("dpx", "SMPTE DPX", r, codecs); + addType("cin", "Kodak Cineon", r, codecs); + addType("cineon", "Kodak Cineon", r, codecs); + addType("jpg", "JPEG image", r, codecs); + addType("jpeg", "JPEG image", r, codecs); + addType("tga", "TARGA", r, codecs); + addType("targa", "TARGA", r, codecs); + addType("rla", "Wavefront RLA", r, codecs); + addType("hdr", "Radiance HDR", r, codecs); + addType("rgbe", "Radiance HDR", r, codecs); + + // Read-only formats addType("psd", "Adobe Photoshop", r, codecs); addType("pic", "Softimage PIC", r, codecs); - addType("tga", "TARGA", rw, codecs); - addType("targa", "TARGA", rw, codecs); + addType("ptex", "Disney PTex", r, codecs); + addType("ptx", "Disney PTex", r, codecs); + addType("gif", "Graphics Interchange Format", r, codecs); + addType("ico", "Palette", r, codecs); + addType("z", "Pixar Z-Depth", r, codecs); + + // Formats written via OpenImageIO (no dedicated native writer plugin) + addType("webp", "Google WebP", rw, codecs); + addType("qoi", "Quite OK Image", rw, codecs); + addType("bmp", "Windows Bitmap", rw, codecs); addType("sgi", "SGI image", rw, codecs); - addType("jpg", "JPEG image", rw, codecs); - addType("jpeg", "JPEG image", rw, codecs); addType("bw", "SGI image", rw, codecs); addType("rgb", "SGI image", rw, codecs); addType("rgba", "SGI image", rw, codecs); addType("inta", "SGI image", rw, codecs); addType("int", "SGI image", rw, codecs); addType("pnm", "PNM", rw, codecs); - addType("fits", "FITS", rw, codecs); - addType("dpx", "SMPTE DPX", rw, codecs); - addType("cin", "Kodak Cineon", rw, codecs); - addType("cineon", "Kodak Cineon", w, codecs); - addType("webp", "Google WebP", rw, codecs); - addType("ptex", "Disney PTex", r, codecs); - addType("ptx", "Disney PTex", r, codecs); - addType("rla", "Wavefront RLA", rw, codecs); - addType("iff", "IFF", rw, codecs); - addType("bmp", "Windows Bitmap", rw, codecs); - addType("dds", "Direct Draw Surface", rw, codecs); - addType("gif", "Graphics Interchange Format", r, codecs); - addType("ico", "Palette", r, codecs); addType("pbm", "Portable Network Graphics", rw, codecs); addType("pgm", "Portable Network Graphics", rw, codecs); addType("ppm", "Portable Network Grapics", rw, codecs); + addType("fits", "FITS", rw, codecs); + addType("iff", "IFF", rw, codecs); + addType("dds", "Direct Draw Surface", rw, codecs); addType("heic", "High Efficiency Image File", rw, codecs); addType("heif", "High Efficiency Image File", rw, codecs); addType("hif", "High Efficiency Image File", rw, codecs); addType("avif", "AV1 Image File", rw, codecs); addType("jxl", "JPEG XL Image", rw, codecs); - addType("hdr", "Radiance HDR", rw, codecs); - addType("rgbe", "Radiance HDR", rw, codecs); - addType("z", "Pixar Z-Depth", r, codecs); - addType("png", "Portable Network Graphics Image", rw, codecs); - addType("qoi", "Quite OK Image", rw, codecs); // These are handled by their dedicated optimized streaming plugins: // IOexr ("m0"), IOtiff ("m1"), IOjpeg ("m2"), IOhtj2k ("m7") @@ -439,7 +448,180 @@ namespace TwkFB void IOoiio::writeImage(const FrameBuffer& img, const std::string& filename, const WriteRequest& request) const { - FrameBufferIO::writeImage(img, filename, request); + const FrameBuffer* outfb = &img; + + // + // Merge planar frames if needed + // + if (outfb->isPlanar()) + { + const FrameBuffer* fb = outfb; + outfb = mergePlanes(outfb); + if (fb != &img) + delete fb; + } + + // + // Convert YUV / Primaries to Linear Rec. 709 if required + // + if (!request.keepColorSpace && (outfb->hasPrimaries() || outfb->isYUV() || outfb->isYRYBY())) + { + const FrameBuffer* fb = outfb; + outfb = convertToLinearRGB709(outfb); + if (fb != &img) + delete fb; + } + + // + // Convert packed or unsupported types to standard formats + // + switch (outfb->dataType()) + { + case FrameBuffer::UCHAR: + case FrameBuffer::USHORT: + case FrameBuffer::UINT: + case FrameBuffer::HALF: + case FrameBuffer::FLOAT: + case FrameBuffer::DOUBLE: + break; + default: + { + const FrameBuffer* fb = outfb; + outfb = copyConvert(outfb, FrameBuffer::UCHAR); + if (fb != &img) + delete fb; + break; + } + } + + // + // Handle orientation: NATURAL (bottom-left origin) needs vertical flip; + // TOPRIGHT / BOTTOMRIGHT needs horizontal flop. + // + bool needflip = false; + bool needflop = false; + + switch (outfb->orientation()) + { + case FrameBuffer::NATURAL: + needflip = true; + break; + case FrameBuffer::TOPRIGHT: + case FrameBuffer::BOTTOMRIGHT: + needflop = true; + break; + default: + break; + } + + if (needflop) + { + if (outfb == &img) + outfb = img.copy(); + flop(const_cast(outfb)); + } + + if (needflip) + { + if (outfb == &img) + outfb = img.copy(); + flip(const_cast(outfb)); + } + + // Ensure temporary FrameBuffer is cleanly deleted upon return/exception + struct FBAutoCleanup + { + const FrameBuffer*& current; + const FrameBuffer& original; + + ~FBAutoCleanup() + { + if (current != &original) + delete current; + } + } autoCleanup{outfb, img}; + + std::unique_ptr out = ImageOutput::create(filename); + if (!out) + { + TWK_THROW_STREAM(IOException, "OIIO: Unable to create output for \"" << filename << "\": " << OIIO::geterror()); + } + + TypeDesc format = TypeDesc::UINT8; + switch (outfb->dataType()) + { + case FrameBuffer::UCHAR: + format = TypeDesc::UINT8; + break; + case FrameBuffer::USHORT: + format = TypeDesc::UINT16; + break; + case FrameBuffer::UINT: + format = TypeDesc::UINT32; + break; + case FrameBuffer::HALF: + format = TypeDesc::HALF; + break; + case FrameBuffer::FLOAT: + format = TypeDesc::FLOAT; + break; + case FrameBuffer::DOUBLE: + format = TypeDesc::DOUBLE; + break; + default: + format = TypeDesc::UINT8; + break; + } + + ImageSpec spec(outfb->width(), outfb->height(), outfb->numChannels(), format); + + if (outfb->channelNames().size() == static_cast(outfb->numChannels())) + { + spec.channelnames.clear(); + for (size_t i = 0; i < outfb->channelNames().size(); ++i) + { + spec.channelnames.push_back(outfb->channelNames()[i]); + } + } + + if (request.pixelAspect != 1.0f && request.pixelAspect != 0.0f) + { + spec.pixelaspect = request.pixelAspect; + } + else if (outfb->pixelAspect() != 1.0f && outfb->pixelAspect() != 0.0f) + { + spec.pixelaspect = outfb->pixelAspect(); + } + + if (!request.compression.empty()) + { + spec.attribute("compression", request.compression); + } + + if (request.quality >= 0.0f && request.quality <= 1.0f) + { + spec.attribute("CompressionQuality", static_cast(request.quality * 100.0f)); + } + + for (const auto& p : request.parameters) + { + spec.attribute(p.first, p.second); + } + + if (!out->open(filename, spec)) + { + TWK_THROW_STREAM(IOException, "OIIO: Unable to open \"" << filename << "\" for writing: " << out->geterror()); + } + + stride_t xstride = outfb->pixelSize(); + stride_t ystride = outfb->scanlinePaddedSize(); + + if (!out->write_image(format, outfb->data(), xstride, ystride)) + { + TWK_THROW_STREAM(IOException, "OIIO: Error writing \"" << filename << "\": " << out->geterror()); + } + + out->close(); } } // End namespace TwkFB diff --git a/src/lib/image/TwkFB/IO.cpp b/src/lib/image/TwkFB/IO.cpp index 8b3d6e02..d2c990db 100644 --- a/src/lib/image/TwkFB/IO.cpp +++ b/src/lib/image/TwkFB/IO.cpp @@ -121,7 +121,8 @@ namespace TwkFB void FrameBufferIO::readImage(FrameBuffer& fb, const string& filename, const ReadRequest& request) const { - throw UnsupportedException(); + TWK_THROW_STREAM(UnsupportedException, + "Image plugin '" << identifier() << "' does not support reading '" << extension(filename) << "'"); } void FrameBufferIO::readImages(FrameBufferVector& fbs, const string& filename, const ReadRequest& request) const @@ -133,7 +134,8 @@ namespace TwkFB void FrameBufferIO::writeImage(const FrameBuffer& img, const string& filename, const WriteRequest& request) const { - throw UnsupportedException(); + TWK_THROW_STREAM(UnsupportedException, + "Image plugin '" << identifier() << "' does not support writing '" << extension(filename) << "'"); } void FrameBufferIO::writeImages(const ConstFrameBufferVector& fbs, const string& filename, const WriteRequest& request) const @@ -144,7 +146,8 @@ namespace TwkFB } else { - throw UnsupportedException(); + TWK_THROW_STREAM(UnsupportedException, "Image plugin '" << identifier() << "' does not support multi-image writing to '" + << extension(filename) << "'"); } } @@ -510,7 +513,7 @@ namespace TwkFB } else { - throw UnsupportedException(); + TWK_THROW_STREAM(UnsupportedException, "No image plugin available to read format '" << format << "'"); } } @@ -525,7 +528,7 @@ namespace TwkFB } else { - throw UnsupportedException(); + TWK_THROW_STREAM(UnsupportedException, "No image plugin available to write format '" << format << "'"); } } @@ -540,7 +543,7 @@ namespace TwkFB } else { - throw UnsupportedException(); + TWK_THROW_STREAM(UnsupportedException, "No image plugin available to write format '" << format << "'"); } } From 276994e2e7c5fbb56e33a8168786db301c55241c Mon Sep 17 00:00:00 2001 From: Michael Oliver Date: Tue, 29 Sep 2026 14:59:05 -0700 Subject: [PATCH 2/5] feat: isolate runtime environment, eliminate global PATH pollution, and add hermetic CLI shims for Windows and macOS --- scripts/install.ps1 | 195 ++++++++--- src/bin/CMakeLists.txt | 5 + src/bin/apps/rv/UTVLauncherWin.cpp | 302 ++++++++++++++---- src/bin/apps/rv/main.cpp | 4 +- src/bin/nsapps/UTV/UTVLauncher.mm | 43 +++ src/bin/nsapps/UTV/openutv-check-updates.cmd | 38 ++- src/bin/nsapps/UTV/openutv-check-updates.sh | 21 +- src/bin/nsapps/UTV/openutv-diagnostics.cmd | 38 ++- src/bin/nsapps/UTV/openutv-diagnostics.sh | 21 +- src/bin/nsapps/UTV/openutv-run.cmd | 52 +++ .../nsapps/UTV/openutv-supercharge-ffmpeg.sh | 21 +- src/bin/nsapps/UTV/py-interp.cmd | 61 ++++ src/bin/nsapps/UTV/utvio.cmd | 63 ++++ src/bin/nsapps/UTV/utvls.cmd | 63 ++++ src/bin/nsapps/UTV/utvpkg.cmd | 63 ++++ src/lib/app/QTBundle/QTBundle.cpp | 6 +- src/lib/app/RvCommon/RvKeybindingsManager.cpp | 7 +- src/lib/app/RvCommon/RvPreferences.cpp | 24 +- src/lib/image/MovieFFMpeg/MovieFFMpeg.cpp | 8 +- 19 files changed, 868 insertions(+), 167 deletions(-) create mode 100644 src/bin/nsapps/UTV/openutv-run.cmd create mode 100644 src/bin/nsapps/UTV/py-interp.cmd create mode 100644 src/bin/nsapps/UTV/utvio.cmd create mode 100644 src/bin/nsapps/UTV/utvls.cmd create mode 100644 src/bin/nsapps/UTV/utvpkg.cmd diff --git a/scripts/install.ps1 b/scripts/install.ps1 index 8ff77c17..33edc617 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -119,6 +119,9 @@ $detectedDepsPath = "" if (Test-Path "C:\Program Files\OpenUTVDeps $DepsVersion\bin\OpenImageIO.dll") { $depsFound = $true $detectedDepsPath = "C:\Program Files\OpenUTVDeps $DepsVersion" +} elseif ($env:UTV_DEPS_ROOT -and (Test-Path "$env:UTV_DEPS_ROOT\bin\OpenImageIO.dll")) { + $depsFound = $true + $detectedDepsPath = $env:UTV_DEPS_ROOT } elseif ($env:OPENUTV_DEPS_ROOT -and (Test-Path "$env:OPENUTV_DEPS_ROOT\bin\OpenImageIO.dll")) { $depsFound = $true $detectedDepsPath = $env:OPENUTV_DEPS_ROOT @@ -209,8 +212,8 @@ if (-not (Test-Path $utvExe)) { return } -# Step 4: Configure PATH and Environment -Write-Host "`n--- Configuring Environment ---" -ForegroundColor Cyan +# Step 4: Configure PATH, Isolated CLI Shims, and Environment +Write-Host "`n--- Configuring Environment & Isolated CLI Tools ---" -ForegroundColor Cyan $binDir = Join-Path $InstallDir "bin" $pathScope = if ($isAdmin) { "Machine" } else { "User" } @@ -222,58 +225,160 @@ if ((Test-Path $legacyQtConf) -and -not (Test-Path $qtPluginDir)) { Write-Host "Removed legacy qt.conf." -ForegroundColor Gray } -# Collect paths to add to PATH -$pathsToAdd = @($binDir) +# Ensure dedicated CLI tools (utvio, utvpkg, py-interp, utvls) have -bin copies and launcher shims +$utvExe = Join-Path $binDir "utv.exe" +$cliTools = @("utvio", "utvpkg", "py-interp", "utvls") +foreach ($tool in $cliTools) { + $toolExe = Join-Path $binDir "$tool.exe" + $toolBin = Join-Path $binDir "$tool-bin.exe" + if ((Test-Path $toolExe) -and -not (Test-Path $toolBin) -and (Test-Path $utvExe)) { + Move-Item -Force -Path $toolExe -Destination $toolBin + Copy-Item -Force -Path $utvExe -Destination $toolExe + Write-Host "Configured hermetic launcher for $tool.exe" -ForegroundColor Gray + } +} -if ($detectedDepsPath -and (Test-Path $detectedDepsPath)) { - $depsBin = Join-Path $detectedDepsPath "bin" - $depsPython = Join-Path $detectedDepsPath "tools\python3" - $depsPySide = Join-Path $detectedDepsPath "tools\python3\Lib\site-packages\PySide6" - $depsPlugins = Join-Path $depsPySide "plugins" - $depsPlatforms = Join-Path $depsPlugins "platforms" +# Deploy companion .cmd scripts for seamless CLI usage without PATH pollution +$cmdShimTemplate = @' +@echo off +setlocal - if (Test-Path $depsBin) { $pathsToAdd += $depsBin } - if (Test-Path $depsPython) { $pathsToAdd += $depsPython } - if (Test-Path $depsPySide) { $pathsToAdd += $depsPySide } +:: Discover OpenUTVDeps runtime root directory +set "DEPS_ROOT=" +if defined UTV_DEPS_ROOT if exist "%UTV_DEPS_ROOT%\bin\OpenImageIO.dll" set "DEPS_ROOT=%UTV_DEPS_ROOT%" +if not defined DEPS_ROOT if defined OPENUTV_DEPS_ROOT if exist "%OPENUTV_DEPS_ROOT%\bin\OpenImageIO.dll" set "DEPS_ROOT=%OPENUTV_DEPS_ROOT%" - # Set persistent OpenUTVDeps environment variables - [Environment]::SetEnvironmentVariable("OPENUTV_DEPS_ROOT", $detectedDepsPath, $pathScope) - $env:OPENUTV_DEPS_ROOT = $detectedDepsPath +if not defined DEPS_ROOT ( + for /f "tokens=2*" %%a in ('reg query "HKCU\Environment" /v "UTV_DEPS_ROOT" 2^>nul') do if exist "%%b\bin\OpenImageIO.dll" set "DEPS_ROOT=%%b" +) +if not defined DEPS_ROOT ( + for /f "tokens=2*" %%a in ('reg query "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment" /v "UTV_DEPS_ROOT" 2^>nul') do if exist "%%b\bin\OpenImageIO.dll" set "DEPS_ROOT=%%b" +) +if not defined DEPS_ROOT ( + for /f "tokens=2*" %%a in ('reg query "HKCU\Environment" /v "OPENUTV_DEPS_ROOT" 2^>nul') do if exist "%%b\bin\OpenImageIO.dll" set "DEPS_ROOT=%%b" +) +if not defined DEPS_ROOT ( + for /f "tokens=2*" %%a in ('reg query "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment" /v "OPENUTV_DEPS_ROOT" 2^>nul') do if exist "%%b\bin\OpenImageIO.dll" set "DEPS_ROOT=%%b" +) +if not defined DEPS_ROOT ( + for /f "tokens=2*" %%a in ('reg query "HKCU\Software\OpenUTV" /v "DepsPath" 2^>nul') do if exist "%%b\bin\OpenImageIO.dll" set "DEPS_ROOT=%%b" +) +if not defined DEPS_ROOT ( + for /f "tokens=2*" %%a in ('reg query "HKLM\Software\OpenUTV" /v "DepsPath" 2^>nul') do if exist "%%b\bin\OpenImageIO.dll" set "DEPS_ROOT=%%b" +) +if not defined DEPS_ROOT ( + for /d %%d in ("%ProgramFiles%\OpenUTVDeps*") do if exist "%%d\bin\OpenImageIO.dll" set "DEPS_ROOT=%%d" +) +if not defined DEPS_ROOT ( + for /d %%d in ("%LOCALAPPDATA%\Programs\OpenUTVDeps*") do if exist "%%d\bin\OpenImageIO.dll" set "DEPS_ROOT=%%d" +) - if (Test-Path $depsPython) { - [Environment]::SetEnvironmentVariable("PYTHONHOME", $depsPython, $pathScope) - $env:PYTHONHOME = $depsPython - } - if (Test-Path $depsPlugins) { - [Environment]::SetEnvironmentVariable("QT_PLUGIN_PATH", $depsPlugins, $pathScope) - $env:QT_PLUGIN_PATH = $depsPlugins - } - if (Test-Path $depsPlatforms) { - [Environment]::SetEnvironmentVariable("QT_QPA_PLATFORM_PLUGIN_PATH", $depsPlatforms, $pathScope) - $env:QT_QPA_PLATFORM_PLUGIN_PATH = $depsPlatforms - } +if not defined DEPS_ROOT ( + echo OpenUTV: Could not locate OpenUTVDeps runtime dependencies. >&2 + exit /b 1 +) + +set "APP_DIR=%~dp0" +if "%APP_DIR:~-1%"=="\" set "APP_DIR=%APP_DIR:~0,-1%" + +set "PATH=%APP_DIR%;%DEPS_ROOT%\bin;%DEPS_ROOT%\tools\python3\Lib\site-packages\PySide6;%DEPS_ROOT%\tools\python3;%DEPS_ROOT%\tools\python3\Scripts;%PATH%" +set "PYTHONHOME=%DEPS_ROOT%\tools\python3" +set "QT_PLUGIN_PATH=%DEPS_ROOT%\tools\python3\Lib\site-packages\PySide6\plugins" +set "QT_QPA_PLATFORM_PLUGIN_PATH=%DEPS_ROOT%\tools\python3\Lib\site-packages\PySide6\plugins\platforms" +set "UTV_DEPS_ROOT=%DEPS_ROOT%" +set "OPENUTV_DEPS_ROOT=%DEPS_ROOT%" +set "UTV_HOME=%APP_DIR%" +set "OPENUTV_HOME=%APP_DIR%" +'@ + +# Write openutv-run.cmd +$openutvRunContent = $cmdShimTemplate + "`r`n`r`n%*`r`nexit /b %ERRORLEVEL%`r`n" +Set-Content -Path (Join-Path $binDir "openutv-run.cmd") -Value $openutvRunContent -Encoding ASCII + +# Write utvio.cmd, utvpkg.cmd, py-interp.cmd, utvls.cmd +foreach ($tool in $cliTools) { + $dispatch = @" + +if exist "%APP_DIR%\$tool-bin.exe" ( + "%APP_DIR%\$tool-bin.exe" %* +) else if exist "%APP_DIR%\$tool.exe" ( + "%APP_DIR%\$tool.exe" %* +) else ( + echo OpenUTV: Could not locate $tool executable. >&2 + exit /b 1 +) +exit /b %ERRORLEVEL% +"@ + Set-Content -Path (Join-Path $binDir "$tool.cmd") -Value ($cmdShimTemplate + $dispatch) -Encoding ASCII } +# Write openutv-diagnostics.cmd and openutv-check-updates.cmd +$diagDispatch = @" + +if exist "%APP_DIR%\py-interp-bin.exe" ( + "%APP_DIR%\py-interp-bin.exe" "%APP_DIR%\openutv-diagnostics.py" %* +) else if exist "%APP_DIR%\py-interp.exe" ( + "%APP_DIR%\py-interp.exe" "%APP_DIR%\openutv-diagnostics.py" %* +) else if exist "%DEPS_ROOT%\tools\python3\python.exe" ( + "%DEPS_ROOT%\tools\python3\python.exe" "%APP_DIR%\openutv-diagnostics.py" %* +) else ( + python "%APP_DIR%\openutv-diagnostics.py" %* +) +exit /b %ERRORLEVEL% +"@ +Set-Content -Path (Join-Path $binDir "openutv-diagnostics.cmd") -Value ($cmdShimTemplate + $diagDispatch) -Encoding ASCII + +$updateDispatch = @" + +if exist "%APP_DIR%\py-interp-bin.exe" ( + "%APP_DIR%\py-interp-bin.exe" "%APP_DIR%\openutv-check-updates.py" %* +) else if exist "%APP_DIR%\py-interp.exe" ( + "%APP_DIR%\py-interp.exe" "%APP_DIR%\openutv-check-updates.py" %* +) else if exist "%DEPS_ROOT%\tools\python3\python.exe" ( + "%DEPS_ROOT%\tools\python3\python.exe" "%APP_DIR%\openutv-check-updates.py" %* +) else ( + python "%APP_DIR%\openutv-check-updates.py" %* +) +exit /b %ERRORLEVEL% +"@ +Set-Content -Path (Join-Path $binDir "openutv-check-updates.cmd") -Value ($cmdShimTemplate + $updateDispatch) -Encoding ASCII + +# Configure persistent environment variables +if ($detectedDepsPath -and (Test-Path $detectedDepsPath)) { + [Environment]::SetEnvironmentVariable("UTV_DEPS_ROOT", $detectedDepsPath, $pathScope) + [Environment]::SetEnvironmentVariable("OPENUTV_DEPS_ROOT", $detectedDepsPath, $pathScope) + $env:UTV_DEPS_ROOT = $detectedDepsPath + $env:OPENUTV_DEPS_ROOT = $detectedDepsPath +} +[Environment]::SetEnvironmentVariable("UTV_HOME", $InstallDir, $pathScope) +[Environment]::SetEnvironmentVariable("OPENUTV_HOME", $InstallDir, $pathScope) +$env:UTV_HOME = $InstallDir +$env:OPENUTV_HOME = $InstallDir + +# Clean global pollution: remove PYTHONHOME, QT_PLUGIN_PATH, QT_QPA_PLATFORM_PLUGIN_PATH +[Environment]::SetEnvironmentVariable("PYTHONHOME", $null, $pathScope) +[Environment]::SetEnvironmentVariable("QT_PLUGIN_PATH", $null, $pathScope) +[Environment]::SetEnvironmentVariable("QT_QPA_PLATFORM_PLUGIN_PATH", $null, $pathScope) +$env:PYTHONHOME = $null +$env:QT_PLUGIN_PATH = $null +$env:QT_QPA_PLATFORM_PLUGIN_PATH = $null + +# Configure PATH: ONLY $binDir, zero pollution from OpenUTVDeps/Scoop/Choco $currentEnvPath = [Environment]::GetEnvironmentVariable("Path", $pathScope) -$currentList = ($currentEnvPath -split ";") | Where-Object { $_ -ne "" } -$newEntries = @() -foreach ($p in $pathsToAdd) { - if ($currentList -notcontains $p) { - $newEntries += $p - } +$cleanList = ($currentEnvPath -split ";") | Where-Object { + $_ -ne "" -and + $_ -notlike "*OpenUTVDeps*" -and + $_ -notlike "*openutv-dependencies*" -and + $_ -ne $binDir } -if ($newEntries.Count -gt 0) { - $newEnvPath = ($newEntries + $currentList) -join ";" - [Environment]::SetEnvironmentVariable("Path", $newEnvPath, $pathScope) - $env:Path = ($newEntries + ($env:Path -split ";")) -join ";" - Write-Host "Added $($newEntries.Count) OpenUTV path(s) to $pathScope PATH." -ForegroundColor Green - foreach ($entry in $newEntries) { - Write-Host " + $entry" -ForegroundColor Gray - } -} else { - Write-Host "OpenUTV binaries and dependencies already present in $pathScope PATH." -ForegroundColor Gray -} +$newEnvPath = (@($binDir) + $cleanList) -join ";" +[Environment]::SetEnvironmentVariable("Path", $newEnvPath, $pathScope) +$env:Path = (@($binDir) + (($env:Path -split ";") | Where-Object { $_ -notlike "*OpenUTVDeps*" -and $_ -notlike "*openutv-dependencies*" -and $_ -ne $binDir })) -join ";" + +Write-Host "Environment configured hermetically (Zero `$PATH pollution)." -ForegroundColor Green +Write-Host " + Added to $pathScope PATH: $binDir" -ForegroundColor Gray +Write-Host " - Cleaned dependency folders and global PYTHONHOME/QT_PLUGIN_PATH from environment." -ForegroundColor Gray # Step 5: Create Start Menu and Desktop Shortcuts if (-not $NoShortcuts) { diff --git a/src/bin/CMakeLists.txt b/src/bin/CMakeLists.txt index 13750458..77e4d7c9 100644 --- a/src/bin/CMakeLists.txt +++ b/src/bin/CMakeLists.txt @@ -23,6 +23,11 @@ CONFIGURE_FILE(${_HELPER_SRC_DIR}/openutv-check-updates.py ${RV_STAGE_BIN_DIR}/o IF(WIN32) CONFIGURE_FILE(${_HELPER_SRC_DIR}/openutv-diagnostics.cmd ${RV_STAGE_BIN_DIR}/openutv-diagnostics.cmd COPYONLY) CONFIGURE_FILE(${_HELPER_SRC_DIR}/openutv-check-updates.cmd ${RV_STAGE_BIN_DIR}/openutv-check-updates.cmd COPYONLY) + CONFIGURE_FILE(${_HELPER_SRC_DIR}/openutv-run.cmd ${RV_STAGE_BIN_DIR}/openutv-run.cmd COPYONLY) + CONFIGURE_FILE(${_HELPER_SRC_DIR}/utvio.cmd ${RV_STAGE_BIN_DIR}/utvio.cmd COPYONLY) + CONFIGURE_FILE(${_HELPER_SRC_DIR}/utvpkg.cmd ${RV_STAGE_BIN_DIR}/utvpkg.cmd COPYONLY) + CONFIGURE_FILE(${_HELPER_SRC_DIR}/py-interp.cmd ${RV_STAGE_BIN_DIR}/py-interp.cmd COPYONLY) + CONFIGURE_FILE(${_HELPER_SRC_DIR}/utvls.cmd ${RV_STAGE_BIN_DIR}/utvls.cmd COPYONLY) ELSE() CONFIGURE_FILE(${_HELPER_SRC_DIR}/openutv-diagnostics.py ${RV_STAGE_BIN_DIR}/openutv-diagnostics COPYONLY) CONFIGURE_FILE(${_HELPER_SRC_DIR}/openutv-check-updates.py ${RV_STAGE_BIN_DIR}/openutv-check-updates COPYONLY) diff --git a/src/bin/apps/rv/UTVLauncherWin.cpp b/src/bin/apps/rv/UTVLauncherWin.cpp index 0210a819..1e750b57 100644 --- a/src/bin/apps/rv/UTVLauncherWin.cpp +++ b/src/bin/apps/rv/UTVLauncherWin.cpp @@ -79,7 +79,8 @@ namespace { // 1. Check if user explicitly requested software GL via environment variable wchar_t envBuf[32]; - if (GetEnvironmentVariableW(L"OPENUTV_SOFTWARE_GL", envBuf, 32) > 0 || GetEnvironmentVariableW(L"QT_OPENGL", envBuf, 32) > 0) + if (GetEnvironmentVariableW(L"UTV_SOFTWARE_GL", envBuf, 32) > 0 || GetEnvironmentVariableW(L"OPENUTV_SOFTWARE_GL", envBuf, 32) > 0 + || GetEnvironmentVariableW(L"QT_OPENGL", envBuf, 32) > 0) { if (_wcsicmp(envBuf, L"software") == 0 || wcscmp(envBuf, L"1") == 0) { @@ -372,17 +373,21 @@ namespace bool FindDependencies(const std::wstring& appDir, std::wstring& outRootDir, std::wstring& outBinDir, std::wstring& outPySideDir, std::wstring& outPythonDir) { - // 1. Environment variable OPENUTV_DEPS_ROOT - DWORD len = GetEnvironmentVariableW(L"OPENUTV_DEPS_ROOT", NULL, 0); - if (len > 0) + // 1. Environment variables: UTV_DEPS_ROOT (primary), OPENUTV_DEPS_ROOT (fallback) + const wchar_t* envVars[] = {L"UTV_DEPS_ROOT", L"OPENUTV_DEPS_ROOT"}; + for (const wchar_t* ev : envVars) { - std::vector buf(len); - GetEnvironmentVariableW(L"OPENUTV_DEPS_ROOT", buf.data(), len); - std::wstring envRoot(buf.data()); - if (CheckDepsDir(envRoot, outBinDir, outPySideDir, outPythonDir)) + DWORD len = GetEnvironmentVariableW(ev, NULL, 0); + if (len > 0) { - outRootDir = envRoot; - return true; + std::vector buf(len); + GetEnvironmentVariableW(ev, buf.data(), len); + std::wstring envRoot(buf.data()); + if (CheckDepsDir(envRoot, outBinDir, outPySideDir, outPythonDir)) + { + outRootDir = envRoot; + return true; + } } } @@ -391,17 +396,20 @@ namespace if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, L"SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment", 0, KEY_READ, &hKey) == ERROR_SUCCESS) { - wchar_t regVal[MAX_PATH]; - DWORD valSize = sizeof(regVal); - DWORD valType = 0; - if (RegQueryValueExW(hKey, L"OPENUTV_DEPS_ROOT", NULL, &valType, reinterpret_cast(regVal), &valSize) == ERROR_SUCCESS) + for (const wchar_t* ev : envVars) { - std::wstring regRoot(regVal); - if (CheckDepsDir(regRoot, outBinDir, outPySideDir, outPythonDir)) + wchar_t regVal[MAX_PATH]; + DWORD valSize = sizeof(regVal); + DWORD valType = 0; + if (RegQueryValueExW(hKey, ev, NULL, &valType, reinterpret_cast(regVal), &valSize) == ERROR_SUCCESS) { - outRootDir = regRoot; - RegCloseKey(hKey); - return true; + std::wstring regRoot(regVal); + if (CheckDepsDir(regRoot, outBinDir, outPySideDir, outPythonDir)) + { + outRootDir = regRoot; + RegCloseKey(hKey); + return true; + } } } RegCloseKey(hKey); @@ -410,23 +418,50 @@ namespace // 3. User Environment in registry if (RegOpenKeyExW(HKEY_CURRENT_USER, L"Environment", 0, KEY_READ, &hKey) == ERROR_SUCCESS) { - wchar_t regVal[MAX_PATH]; - DWORD valSize = sizeof(regVal); - DWORD valType = 0; - if (RegQueryValueExW(hKey, L"OPENUTV_DEPS_ROOT", NULL, &valType, reinterpret_cast(regVal), &valSize) == ERROR_SUCCESS) + for (const wchar_t* ev : envVars) { - std::wstring regRoot(regVal); - if (CheckDepsDir(regRoot, outBinDir, outPySideDir, outPythonDir)) + wchar_t regVal[MAX_PATH]; + DWORD valSize = sizeof(regVal); + DWORD valType = 0; + if (RegQueryValueExW(hKey, ev, NULL, &valType, reinterpret_cast(regVal), &valSize) == ERROR_SUCCESS) { - outRootDir = regRoot; - RegCloseKey(hKey); - return true; + std::wstring regRoot(regVal); + if (CheckDepsDir(regRoot, outBinDir, outPySideDir, outPythonDir)) + { + outRootDir = regRoot; + RegCloseKey(hKey); + return true; + } } } RegCloseKey(hKey); } - // 4. Windows Uninstall registry keys (detects any installed OpenUTVDeps MSI package) + // 4. Software\OpenUTV registry keys (DepsPath) + const HKEY appRootKeys[] = {HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE}; + for (HKEY rk : appRootKeys) + { + HKEY hAppKey = NULL; + if (RegOpenKeyExW(rk, L"Software\\OpenUTV", 0, KEY_READ, &hAppKey) == ERROR_SUCCESS) + { + wchar_t regVal[MAX_PATH]; + DWORD valSize = sizeof(regVal); + DWORD valType = 0; + if (RegQueryValueExW(hAppKey, L"DepsPath", NULL, &valType, reinterpret_cast(regVal), &valSize) == ERROR_SUCCESS) + { + std::wstring regRoot(regVal); + if (CheckDepsDir(regRoot, outBinDir, outPySideDir, outPythonDir)) + { + outRootDir = regRoot; + RegCloseKey(hAppKey); + return true; + } + } + RegCloseKey(hAppKey); + } + } + + // 5. Windows Uninstall registry keys (detects any installed OpenUTVDeps MSI package) const HKEY rootKeys[] = {HKEY_LOCAL_MACHINE, HKEY_CURRENT_USER}; const wchar_t* subKeyPaths[] = {L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall", L"SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall"}; @@ -668,8 +703,18 @@ namespace int RunLauncher() { - // Try to attach to parent console if running from cmd.exe or PowerShell - AttachConsole(ATTACH_PARENT_PROCESS); + // Attach to parent console if running from cmd.exe or PowerShell + HANDLE hExistingStdOut = GetStdHandle(STD_OUTPUT_HANDLE); + if (hExistingStdOut == NULL || hExistingStdOut == INVALID_HANDLE_VALUE) + { + if (AttachConsole(ATTACH_PARENT_PROCESS)) + { + FILE* fp = nullptr; + freopen_s(&fp, "CONOUT$", "w", stdout); + freopen_s(&fp, "CONOUT$", "w", stderr); + freopen_s(&fp, "CONIN$", "r", stdin); + } + } std::wstring appDir = GetAppDir(); std::wstring depsRoot; @@ -718,12 +763,18 @@ int RunLauncher() } } - // Export OPENUTV_DEPS_ROOT and OPENUTV_DEPS_ROOT_SLASH for runtime child processes + // Export UTV_DEPS_ROOT and OPENUTV_DEPS_ROOT for runtime child processes + SetEnvironmentVariableW(L"UTV_DEPS_ROOT", depsRoot.c_str()); SetEnvironmentVariableW(L"OPENUTV_DEPS_ROOT", depsRoot.c_str()); std::wstring rootSlash = depsRoot; std::replace(rootSlash.begin(), rootSlash.end(), L'\\', L'/'); + SetEnvironmentVariableW(L"UTV_DEPS_ROOT_SLASH", rootSlash.c_str()); SetEnvironmentVariableW(L"OPENUTV_DEPS_ROOT_SLASH", rootSlash.c_str()); + // Export UTV_HOME and OPENUTV_HOME + SetEnvironmentVariableW(L"UTV_HOME", appDir.c_str()); + SetEnvironmentVariableW(L"OPENUTV_HOME", appDir.c_str()); + // Prepend dependency paths to process PATH std::wstring currentPath; DWORD pathLen = GetEnvironmentVariableW(L"PATH", NULL, 0); @@ -890,57 +941,175 @@ int RunLauncher() SetEnvironmentVariableW(L"QT_OPENGL", L"desktop"); } - // Locate core application executable: utv-bin.exe (or rv-bin.exe) - std::wstring targetExe = appDir + L"\\utv-bin.exe"; - if (!FileExists(targetExe)) + // Parse command line arguments for target resolution + int argc = 0; + LPWSTR* argv = CommandLineToArgvW(GetCommandLineW(), &argc); + std::wstring targetExe; + std::wstring cmdLine; + + if (argv && argc >= 3 && _wcsicmp(argv[1], L"--run") == 0) { - targetExe = appDir + L"\\rv-bin.exe"; - if (!FileExists(targetExe)) + // Explicit tool execution mode: utv --run [arguments...] + std::wstring cmd = argv[2]; + if (FileExists(cmd)) { - MessageBoxW(NULL, L"Unable to locate utv-bin.exe or rv-bin.exe in the application directory.", L"OpenUTV Launcher Error", - MB_ICONERROR | MB_OK); - return 1; + targetExe = cmd; } - } - - // Build command line preserving all caller arguments - std::wstring cmdLine = L"\"" + targetExe + L"\""; - const wchar_t* rawCmd = GetCommandLineW(); - if (rawCmd) - { - const wchar_t* p = rawCmd; - while (*p == L' ' || *p == L'\t') + else if (FileExists(cmd + L".exe")) + { + targetExe = cmd + L".exe"; + } + else if (FileExists(appDir + L"\\" + cmd)) + { + targetExe = appDir + L"\\" + cmd; + } + else if (FileExists(appDir + L"\\" + cmd + L".exe")) + { + targetExe = appDir + L"\\" + cmd + L".exe"; + } + else if (FileExists(appDir + L"\\" + cmd + L"-bin.exe")) { - p++; + targetExe = appDir + L"\\" + cmd + L"-bin.exe"; } - if (*p == L'"') + else if (!depsBin.empty() && FileExists(depsBin + L"\\" + cmd)) { - p++; - while (*p && *p != L'"') + targetExe = depsBin + L"\\" + cmd; + } + else if (!depsBin.empty() && FileExists(depsBin + L"\\" + cmd + L".exe")) + { + targetExe = depsBin + L"\\" + cmd + L".exe"; + } + else if (!depsPython.empty() && FileExists(depsPython + L"\\" + cmd)) + { + targetExe = depsPython + L"\\" + cmd; + } + else if (!depsPython.empty() && FileExists(depsPython + L"\\" + cmd + L".exe")) + { + targetExe = depsPython + L"\\" + cmd + L".exe"; + } + else + { + wchar_t found[MAX_PATH]; + LPWSTR fPart = NULL; + if (SearchPathW(NULL, cmd.c_str(), L".exe", MAX_PATH, found, &fPart) > 0) { - p++; + targetExe = found; } - if (*p == L'"') + else { - p++; + targetExe = cmd; } } - else + + cmdLine = L"\"" + targetExe + L"\""; + for (int i = 3; i < argc; ++i) { - while (*p && *p != L' ' && *p != L'\t') + cmdLine += L" "; + if (wcschr(argv[i], L' ') != nullptr || wcschr(argv[i], L'\t') != nullptr) { - p++; + cmdLine += L"\""; + cmdLine += argv[i]; + cmdLine += L"\""; + } + else + { + cmdLine += argv[i]; } } - while (*p == L' ' || *p == L'\t') + } + else + { + // Detect launcher own executable name (e.g. utv.exe, utvio.exe, utvpkg.exe, py-interp.exe) + std::vector modBuf(MAX_PATH); + DWORD mLen = GetModuleFileNameW(NULL, modBuf.data(), static_cast(modBuf.size())); + while (mLen >= modBuf.size()) { - p++; + modBuf.resize(modBuf.size() * 2); + mLen = GetModuleFileNameW(NULL, modBuf.data(), static_cast(modBuf.size())); } - if (*p) + std::wstring fullMod(modBuf.data(), mLen); + size_t slash = fullMod.find_last_of(L"\\/"); + std::wstring ownName = (slash != std::wstring::npos) ? fullMod.substr(slash + 1) : fullMod; + std::wstring baseName = ownName; + if (baseName.length() >= 4 && _wcsicmp(baseName.c_str() + baseName.length() - 4, L".exe") == 0) { - cmdLine += L" "; - cmdLine += p; + baseName = baseName.substr(0, baseName.length() - 4); } + + if (_wcsicmp(baseName.c_str(), L"utv") != 0 && _wcsicmp(baseName.c_str(), L"rv") != 0) + { + // Dedicated tool launcher shim (e.g. utvio, utvpkg, py-interp, utvls) + std::wstring candBin = appDir + L"\\" + baseName + L"-bin.exe"; + if (FileExists(candBin)) + { + targetExe = candBin; + } + else if (_wcsicmp(baseName.c_str(), L"py-interp") == 0 && !depsPython.empty() && FileExists(depsPython + L"\\python.exe")) + { + targetExe = depsPython + L"\\python.exe"; + } + else + { + std::wstring candExe = appDir + L"\\" + baseName + L".exe"; + if (FileExists(candExe) && _wcsicmp(candExe.c_str(), fullMod.c_str()) != 0) + { + targetExe = candExe; + } + } + } + + if (targetExe.empty()) + { + // Default OpenUTV / OpenRV viewer binary + targetExe = appDir + L"\\utv-bin.exe"; + if (!FileExists(targetExe)) + { + targetExe = appDir + L"\\rv-bin.exe"; + if (!FileExists(targetExe)) + { + MessageBoxW(NULL, L"Unable to locate utv-bin.exe or rv-bin.exe in the application directory.", + L"OpenUTV Launcher Error", MB_ICONERROR | MB_OK); + if (argv) + LocalFree(argv); + return 1; + } + } + } + + // Build command line preserving caller arguments + cmdLine = L"\"" + targetExe + L"\""; + const wchar_t* rawCmd = GetCommandLineW(); + if (rawCmd) + { + const wchar_t* p = rawCmd; + while (*p == L' ' || *p == L'\t') + p++; + if (*p == L'"') + { + p++; + while (*p && *p != L'"') + p++; + if (*p == L'"') + p++; + } + else + { + while (*p && *p != L' ' && *p != L'\t') + p++; + } + while (*p == L' ' || *p == L'\t') + p++; + if (*p) + { + cmdLine += L" "; + cmdLine += p; + } + } + } + + if (argv) + { + LocalFree(argv); } std::vector cmdBuf(cmdLine.begin(), cmdLine.end()); @@ -972,8 +1141,9 @@ int RunLauncher() if (!created) { DWORD err = GetLastError(); - wchar_t errMsg[256]; - swprintf_s(errMsg, 256, L"Failed to launch core application (utv-bin.exe).\nError code: %lu", err); + wchar_t errMsg[512]; + swprintf_s(errMsg, 512, L"Failed to launch '%s'.\nError code: %lu", targetExe.c_str(), err); + fwprintf(stderr, L"OpenUTV Launcher Error: Failed to launch '%s' (error code: %lu)\n", targetExe.c_str(), err); MessageBoxW(NULL, errMsg, L"OpenUTV Launcher Error", MB_ICONERROR | MB_OK); return 1; } diff --git a/src/bin/apps/rv/main.cpp b/src/bin/apps/rv/main.cpp index a57a658a..77ed13d7 100644 --- a/src/bin/apps/rv/main.cpp +++ b/src/bin/apps/rv/main.cpp @@ -248,7 +248,8 @@ int utf8Main(int argc, char* argv[]) // Verify OpenUTVDeps is installed or discoverable before initializing the runtime bool depsFound = false; char depsRootBuf[MAX_PATH] = {0}; - if (GetEnvironmentVariableA("OPENUTV_DEPS_ROOT", depsRootBuf, MAX_PATH) > 0) + if (GetEnvironmentVariableA("UTV_DEPS_ROOT", depsRootBuf, MAX_PATH) > 0 + || GetEnvironmentVariableA("OPENUTV_DEPS_ROOT", depsRootBuf, MAX_PATH) > 0) { std::string testPath = std::string(depsRootBuf) + "\\bin"; DWORD attr = GetFileAttributesA(testPath.c_str()); @@ -291,6 +292,7 @@ int utf8Main(int argc, char* argv[]) if (attr != INVALID_FILE_ATTRIBUTES && (attr & FILE_ATTRIBUTE_DIRECTORY)) { depsFound = true; + SetEnvironmentVariableA("UTV_DEPS_ROOT", candidate.c_str()); SetEnvironmentVariableA("OPENUTV_DEPS_ROOT", candidate.c_str()); char currentPath[32767] = {0}; GetEnvironmentVariableA("PATH", currentPath, 32767); diff --git a/src/bin/nsapps/UTV/UTVLauncher.mm b/src/bin/nsapps/UTV/UTVLauncher.mm index 20e307ed..55c48e5a 100644 --- a/src/bin/nsapps/UTV/UTVLauncher.mm +++ b/src/bin/nsapps/UTV/UTVLauncher.mm @@ -42,6 +42,16 @@ }; static NSString *detectBrewPrefix(void) { + const char *customDeps = getenv("UTV_DEPS_ROOT"); + if (!customDeps) customDeps = getenv("OPENUTV_DEPS_ROOT"); + if (!customDeps) customDeps = getenv("HOMEBREW_PREFIX"); + if (customDeps && strlen(customDeps) > 0) { + NSString *prefix = [NSString stringWithUTF8String:customDeps]; + if ([[NSFileManager defaultManager] fileExistsAtPath:prefix]) { + return prefix; + } + } + NSFileManager *fm = [NSFileManager defaultManager]; if ([fm fileExistsAtPath:@"/opt/homebrew/bin/brew"]) { return @"/opt/homebrew"; @@ -328,6 +338,39 @@ int main(int argc, char *argv[]) { // All dependencies satisfied -> launch real binary immediately if ([missing count] == 0) { + // Set UTV environment variables for runtime child processes + if (getenv("UTV_DEPS_ROOT") == NULL) { + setenv("UTV_DEPS_ROOT", [brewPrefix UTF8String], 1); + } + if (getenv("OPENUTV_DEPS_ROOT") == NULL) { + setenv("OPENUTV_DEPS_ROOT", [brewPrefix UTF8String], 1); + } + if (getenv("UTV_HOME") == NULL) { + NSString *bundlePath = [[NSBundle mainBundle] bundlePath]; + if (bundlePath) { + setenv("UTV_HOME", [bundlePath UTF8String], 1); + setenv("OPENUTV_HOME", [bundlePath UTF8String], 1); + } + } + + // Check if --run [args...] mode was requested + if (argc >= 3 && strcmp(argv[1], "--run") == 0) { + NSString *targetCmd = [NSString stringWithUTF8String:argv[2]]; + NSString *binDir = [[[NSBundle mainBundle] executablePath] stringByDeletingLastPathComponent]; + NSString *targetPath = [binDir stringByAppendingPathComponent:targetCmd]; + if (![[NSFileManager defaultManager] fileExistsAtPath:targetPath]) { + targetPath = [[brewPrefix stringByAppendingPathComponent:@"bin"] stringByAppendingPathComponent:targetCmd]; + } + if ([[NSFileManager defaultManager] fileExistsAtPath:targetPath]) { + execv([targetPath UTF8String], &argv[2]); + perror("UTVLauncher: execv --run failed"); + return 1; + } else { + fprintf(stderr, "UTVLauncher: Command '%s' not found.\n", argv[2]); + return 1; + } + } + NSString *realBin = findRealBinary(); if (realBin) { execv([realBin UTF8String], argv); diff --git a/src/bin/nsapps/UTV/openutv-check-updates.cmd b/src/bin/nsapps/UTV/openutv-check-updates.cmd index eacac02c..60d03e16 100644 --- a/src/bin/nsapps/UTV/openutv-check-updates.cmd +++ b/src/bin/nsapps/UTV/openutv-check-updates.cmd @@ -3,24 +3,38 @@ setlocal enabledelayedexpansion set "DIR=%~dp0" :: 1. Discover OpenUTVDeps Root -if not defined OPENUTV_DEPS_ROOT ( - for /f "tokens=2*" %%a in ('reg query "HKCU\Software\OpenUTV" /v "DepsPath" 2^>nul') do set "OPENUTV_DEPS_ROOT=%%b" +if not defined UTV_DEPS_ROOT if defined OPENUTV_DEPS_ROOT set "UTV_DEPS_ROOT=%OPENUTV_DEPS_ROOT%" +if not defined UTV_DEPS_ROOT ( + for /f "tokens=2*" %%a in ('reg query "HKCU\Environment" /v "UTV_DEPS_ROOT" 2^>nul') do set "UTV_DEPS_ROOT=%%b" ) -if not defined OPENUTV_DEPS_ROOT ( - for /f "tokens=2*" %%a in ('reg query "HKLM\Software\OpenUTV" /v "DepsPath" 2^>nul') do set "OPENUTV_DEPS_ROOT=%%b" +if not defined UTV_DEPS_ROOT ( + for /f "tokens=2*" %%a in ('reg query "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment" /v "UTV_DEPS_ROOT" 2^>nul') do set "UTV_DEPS_ROOT=%%b" ) -if not defined OPENUTV_DEPS_ROOT ( - for /d %%d in ("%ProgramFiles%\OpenUTVDeps*") do if exist "%%d\bin\OpenImageIO.dll" set "OPENUTV_DEPS_ROOT=%%d" +if not defined UTV_DEPS_ROOT ( + for /f "tokens=2*" %%a in ('reg query "HKCU\Environment" /v "OPENUTV_DEPS_ROOT" 2^>nul') do set "UTV_DEPS_ROOT=%%b" ) -if not defined OPENUTV_DEPS_ROOT ( - for /d %%d in ("%LOCALAPPDATA%\Programs\OpenUTVDeps*") do if exist "%%d\bin\OpenImageIO.dll" set "OPENUTV_DEPS_ROOT=%%d" +if not defined UTV_DEPS_ROOT ( + for /f "tokens=2*" %%a in ('reg query "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment" /v "OPENUTV_DEPS_ROOT" 2^>nul') do set "UTV_DEPS_ROOT=%%b" ) +if not defined UTV_DEPS_ROOT ( + for /f "tokens=2*" %%a in ('reg query "HKCU\Software\OpenUTV" /v "DepsPath" 2^>nul') do set "UTV_DEPS_ROOT=%%b" +) +if not defined UTV_DEPS_ROOT ( + for /f "tokens=2*" %%a in ('reg query "HKLM\Software\OpenUTV" /v "DepsPath" 2^>nul') do set "UTV_DEPS_ROOT=%%b" +) +if not defined UTV_DEPS_ROOT ( + for /d %%d in ("%ProgramFiles%\OpenUTVDeps*") do if exist "%%d\bin\OpenImageIO.dll" set "UTV_DEPS_ROOT=%%d" +) +if not defined UTV_DEPS_ROOT ( + for /d %%d in ("%LOCALAPPDATA%\Programs\OpenUTVDeps*") do if exist "%%d\bin\OpenImageIO.dll" set "UTV_DEPS_ROOT=%%d" +) +if not defined OPENUTV_DEPS_ROOT set "OPENUTV_DEPS_ROOT=%UTV_DEPS_ROOT%" :: 2. Configure Environment if Dependencies Found -if defined OPENUTV_DEPS_ROOT ( - set "DEPS_BIN=%OPENUTV_DEPS_ROOT%\bin" - set "DEPS_PY=%OPENUTV_DEPS_ROOT%\tools\python3" - set "DEPS_PYSIDE=%OPENUTV_DEPS_ROOT%\tools\python3\Lib\site-packages\PySide6" +if defined UTV_DEPS_ROOT ( + set "DEPS_BIN=%UTV_DEPS_ROOT%\bin" + set "DEPS_PY=%UTV_DEPS_ROOT%\tools\python3" + set "DEPS_PYSIDE=%UTV_DEPS_ROOT%\tools\python3\Lib\site-packages\PySide6" set "PATH=!DEPS_BIN!;!DEPS_PY!;!DEPS_PYSIDE!;%DIR%;!PATH!" if not defined PYTHONHOME set "PYTHONHOME=!DEPS_PY!" if not defined QT_PLUGIN_PATH if exist "!DEPS_PYSIDE!\plugins" set "QT_PLUGIN_PATH=!DEPS_PYSIDE!\plugins" diff --git a/src/bin/nsapps/UTV/openutv-check-updates.sh b/src/bin/nsapps/UTV/openutv-check-updates.sh index ad2c9f16..7da1e673 100755 --- a/src/bin/nsapps/UTV/openutv-check-updates.sh +++ b/src/bin/nsapps/UTV/openutv-check-updates.sh @@ -1,9 +1,24 @@ #!/bin/bash +# OpenUTV Check Updates Wrapper + +# Ensure Homebrew environment is available +if [ -f "/opt/homebrew/bin/brew" ]; then + eval "$(/opt/homebrew/bin/brew shellenv 2>/dev/null)" +elif [ -f "/usr/local/bin/brew" ]; then + eval "$(/usr/local/bin/brew shellenv 2>/dev/null)" +fi + DIR="$(cd "$(dirname "$0")" && pwd)" +SCRIPT="${DIR}/openutv-check-updates.py" + if [ -x "${DIR}/py-interp" ]; then - exec "${DIR}/py-interp" "${DIR}/openutv-check-updates.py" "$@" + exec "${DIR}/py-interp" "${SCRIPT}" "$@" +elif [ -x "${DIR}/../MacOS/py-interp" ]; then + exec "${DIR}/../MacOS/py-interp" "${SCRIPT}" "$@" +elif [ -x "${DIR}/../bin/py-interp" ]; then + exec "${DIR}/../bin/py-interp" "${SCRIPT}" "$@" elif command -v python3 &>/dev/null; then - exec python3 "${DIR}/openutv-check-updates.py" "$@" + exec python3 "${SCRIPT}" "$@" else - exec python "${DIR}/openutv-check-updates.py" "$@" + exec python "${SCRIPT}" "$@" fi diff --git a/src/bin/nsapps/UTV/openutv-diagnostics.cmd b/src/bin/nsapps/UTV/openutv-diagnostics.cmd index 091ad3f9..ddc40a4a 100644 --- a/src/bin/nsapps/UTV/openutv-diagnostics.cmd +++ b/src/bin/nsapps/UTV/openutv-diagnostics.cmd @@ -3,24 +3,38 @@ setlocal enabledelayedexpansion set "DIR=%~dp0" :: 1. Discover OpenUTVDeps Root -if not defined OPENUTV_DEPS_ROOT ( - for /f "tokens=2*" %%a in ('reg query "HKCU\Software\OpenUTV" /v "DepsPath" 2^>nul') do set "OPENUTV_DEPS_ROOT=%%b" +if not defined UTV_DEPS_ROOT if defined OPENUTV_DEPS_ROOT set "UTV_DEPS_ROOT=%OPENUTV_DEPS_ROOT%" +if not defined UTV_DEPS_ROOT ( + for /f "tokens=2*" %%a in ('reg query "HKCU\Environment" /v "UTV_DEPS_ROOT" 2^>nul') do set "UTV_DEPS_ROOT=%%b" ) -if not defined OPENUTV_DEPS_ROOT ( - for /f "tokens=2*" %%a in ('reg query "HKLM\Software\OpenUTV" /v "DepsPath" 2^>nul') do set "OPENUTV_DEPS_ROOT=%%b" +if not defined UTV_DEPS_ROOT ( + for /f "tokens=2*" %%a in ('reg query "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment" /v "UTV_DEPS_ROOT" 2^>nul') do set "UTV_DEPS_ROOT=%%b" ) -if not defined OPENUTV_DEPS_ROOT ( - for /d %%d in ("%ProgramFiles%\OpenUTVDeps*") do if exist "%%d\bin\OpenImageIO.dll" set "OPENUTV_DEPS_ROOT=%%d" +if not defined UTV_DEPS_ROOT ( + for /f "tokens=2*" %%a in ('reg query "HKCU\Environment" /v "OPENUTV_DEPS_ROOT" 2^>nul') do set "UTV_DEPS_ROOT=%%b" ) -if not defined OPENUTV_DEPS_ROOT ( - for /d %%d in ("%LOCALAPPDATA%\Programs\OpenUTVDeps*") do if exist "%%d\bin\OpenImageIO.dll" set "OPENUTV_DEPS_ROOT=%%d" +if not defined UTV_DEPS_ROOT ( + for /f "tokens=2*" %%a in ('reg query "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment" /v "OPENUTV_DEPS_ROOT" 2^>nul') do set "UTV_DEPS_ROOT=%%b" ) +if not defined UTV_DEPS_ROOT ( + for /f "tokens=2*" %%a in ('reg query "HKCU\Software\OpenUTV" /v "DepsPath" 2^>nul') do set "UTV_DEPS_ROOT=%%b" +) +if not defined UTV_DEPS_ROOT ( + for /f "tokens=2*" %%a in ('reg query "HKLM\Software\OpenUTV" /v "DepsPath" 2^>nul') do set "UTV_DEPS_ROOT=%%b" +) +if not defined UTV_DEPS_ROOT ( + for /d %%d in ("%ProgramFiles%\OpenUTVDeps*") do if exist "%%d\bin\OpenImageIO.dll" set "UTV_DEPS_ROOT=%%d" +) +if not defined UTV_DEPS_ROOT ( + for /d %%d in ("%LOCALAPPDATA%\Programs\OpenUTVDeps*") do if exist "%%d\bin\OpenImageIO.dll" set "UTV_DEPS_ROOT=%%d" +) +if not defined OPENUTV_DEPS_ROOT set "OPENUTV_DEPS_ROOT=%UTV_DEPS_ROOT%" :: 2. Configure Environment if Dependencies Found -if defined OPENUTV_DEPS_ROOT ( - set "DEPS_BIN=%OPENUTV_DEPS_ROOT%\bin" - set "DEPS_PY=%OPENUTV_DEPS_ROOT%\tools\python3" - set "DEPS_PYSIDE=%OPENUTV_DEPS_ROOT%\tools\python3\Lib\site-packages\PySide6" +if defined UTV_DEPS_ROOT ( + set "DEPS_BIN=%UTV_DEPS_ROOT%\bin" + set "DEPS_PY=%UTV_DEPS_ROOT%\tools\python3" + set "DEPS_PYSIDE=%UTV_DEPS_ROOT%\tools\python3\Lib\site-packages\PySide6" set "PATH=!DEPS_BIN!;!DEPS_PY!;!DEPS_PYSIDE!;%DIR%;!PATH!" if not defined PYTHONHOME set "PYTHONHOME=!DEPS_PY!" if not defined QT_PLUGIN_PATH if exist "!DEPS_PYSIDE!\plugins" set "QT_PLUGIN_PATH=!DEPS_PYSIDE!\plugins" diff --git a/src/bin/nsapps/UTV/openutv-diagnostics.sh b/src/bin/nsapps/UTV/openutv-diagnostics.sh index 3ac2a4de..4fac5f03 100755 --- a/src/bin/nsapps/UTV/openutv-diagnostics.sh +++ b/src/bin/nsapps/UTV/openutv-diagnostics.sh @@ -1,9 +1,24 @@ #!/bin/bash +# OpenUTV Diagnostics Wrapper + +# Ensure Homebrew environment is available +if [ -f "/opt/homebrew/bin/brew" ]; then + eval "$(/opt/homebrew/bin/brew shellenv 2>/dev/null)" +elif [ -f "/usr/local/bin/brew" ]; then + eval "$(/usr/local/bin/brew shellenv 2>/dev/null)" +fi + DIR="$(cd "$(dirname "$0")" && pwd)" +SCRIPT="${DIR}/openutv-diagnostics.py" + if [ -x "${DIR}/py-interp" ]; then - exec "${DIR}/py-interp" "${DIR}/openutv-diagnostics.py" "$@" + exec "${DIR}/py-interp" "${SCRIPT}" "$@" +elif [ -x "${DIR}/../MacOS/py-interp" ]; then + exec "${DIR}/../MacOS/py-interp" "${SCRIPT}" "$@" +elif [ -x "${DIR}/../bin/py-interp" ]; then + exec "${DIR}/../bin/py-interp" "${SCRIPT}" "$@" elif command -v python3 &>/dev/null; then - exec python3 "${DIR}/openutv-diagnostics.py" "$@" + exec python3 "${SCRIPT}" "$@" else - exec python "${DIR}/openutv-diagnostics.py" "$@" + exec python "${SCRIPT}" "$@" fi diff --git a/src/bin/nsapps/UTV/openutv-run.cmd b/src/bin/nsapps/UTV/openutv-run.cmd new file mode 100644 index 00000000..ba96ef09 --- /dev/null +++ b/src/bin/nsapps/UTV/openutv-run.cmd @@ -0,0 +1,52 @@ +@echo off +setlocal + +:: Discover OpenUTVDeps runtime root directory +set "DEPS_ROOT=" +if defined UTV_DEPS_ROOT if exist "%UTV_DEPS_ROOT%\bin\OpenImageIO.dll" set "DEPS_ROOT=%UTV_DEPS_ROOT%" +if not defined DEPS_ROOT if defined OPENUTV_DEPS_ROOT if exist "%OPENUTV_DEPS_ROOT%\bin\OpenImageIO.dll" set "DEPS_ROOT=%OPENUTV_DEPS_ROOT%" + +if not defined DEPS_ROOT ( + for /f "tokens=2*" %%a in ('reg query "HKCU\Environment" /v "UTV_DEPS_ROOT" 2^>nul') do if exist "%%b\bin\OpenImageIO.dll" set "DEPS_ROOT=%%b" +) +if not defined DEPS_ROOT ( + for /f "tokens=2*" %%a in ('reg query "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment" /v "UTV_DEPS_ROOT" 2^>nul') do if exist "%%b\bin\OpenImageIO.dll" set "DEPS_ROOT=%%b" +) +if not defined DEPS_ROOT ( + for /f "tokens=2*" %%a in ('reg query "HKCU\Environment" /v "OPENUTV_DEPS_ROOT" 2^>nul') do if exist "%%b\bin\OpenImageIO.dll" set "DEPS_ROOT=%%b" +) +if not defined DEPS_ROOT ( + for /f "tokens=2*" %%a in ('reg query "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment" /v "OPENUTV_DEPS_ROOT" 2^>nul') do if exist "%%b\bin\OpenImageIO.dll" set "DEPS_ROOT=%%b" +) +if not defined DEPS_ROOT ( + for /f "tokens=2*" %%a in ('reg query "HKCU\Software\OpenUTV" /v "DepsPath" 2^>nul') do if exist "%%b\bin\OpenImageIO.dll" set "DEPS_ROOT=%%b" +) +if not defined DEPS_ROOT ( + for /f "tokens=2*" %%a in ('reg query "HKLM\Software\OpenUTV" /v "DepsPath" 2^>nul') do if exist "%%b\bin\OpenImageIO.dll" set "DEPS_ROOT=%%b" +) +if not defined DEPS_ROOT ( + for /d %%d in ("%ProgramFiles%\OpenUTVDeps*") do if exist "%%d\bin\OpenImageIO.dll" set "DEPS_ROOT=%%d" +) +if not defined DEPS_ROOT ( + for /d %%d in ("%LOCALAPPDATA%\Programs\OpenUTVDeps*") do if exist "%%d\bin\OpenImageIO.dll" set "DEPS_ROOT=%%d" +) + +if not defined DEPS_ROOT ( + echo OpenUTV: Could not locate OpenUTVDeps runtime dependencies. >&2 + exit /b 1 +) + +set "APP_DIR=%~dp0" +if "%APP_DIR:~-1%"=="\" set "APP_DIR=%APP_DIR:~0,-1%" + +set "PATH=%APP_DIR%;%DEPS_ROOT%\bin;%DEPS_ROOT%\tools\python3\Lib\site-packages\PySide6;%DEPS_ROOT%\tools\python3;%DEPS_ROOT%\tools\python3\Scripts;%PATH%" +set "PYTHONHOME=%DEPS_ROOT%\tools\python3" +set "QT_PLUGIN_PATH=%DEPS_ROOT%\tools\python3\Lib\site-packages\PySide6\plugins" +set "QT_QPA_PLATFORM_PLUGIN_PATH=%DEPS_ROOT%\tools\python3\Lib\site-packages\PySide6\plugins\platforms" +set "UTV_DEPS_ROOT=%DEPS_ROOT%" +set "OPENUTV_DEPS_ROOT=%DEPS_ROOT%" +set "UTV_HOME=%APP_DIR%" +set "OPENUTV_HOME=%APP_DIR%" + +%* +exit /b %ERRORLEVEL% diff --git a/src/bin/nsapps/UTV/openutv-supercharge-ffmpeg.sh b/src/bin/nsapps/UTV/openutv-supercharge-ffmpeg.sh index b2870e91..be4bc5b3 100755 --- a/src/bin/nsapps/UTV/openutv-supercharge-ffmpeg.sh +++ b/src/bin/nsapps/UTV/openutv-supercharge-ffmpeg.sh @@ -1,9 +1,24 @@ #!/bin/bash +# OpenUTV Supercharge FFmpeg Wrapper + +# Ensure Homebrew environment is available +if [ -f "/opt/homebrew/bin/brew" ]; then + eval "$(/opt/homebrew/bin/brew shellenv 2>/dev/null)" +elif [ -f "/usr/local/bin/brew" ]; then + eval "$(/usr/local/bin/brew shellenv 2>/dev/null)" +fi + DIR="$(cd "$(dirname "$0")" && pwd)" +SCRIPT="${DIR}/openutv-supercharge-ffmpeg.py" + if [ -x "${DIR}/py-interp" ]; then - exec "${DIR}/py-interp" "${DIR}/openutv-supercharge-ffmpeg.py" "$@" + exec "${DIR}/py-interp" "${SCRIPT}" "$@" +elif [ -x "${DIR}/../MacOS/py-interp" ]; then + exec "${DIR}/../MacOS/py-interp" "${SCRIPT}" "$@" +elif [ -x "${DIR}/../bin/py-interp" ]; then + exec "${DIR}/../bin/py-interp" "${SCRIPT}" "$@" elif command -v python3 &>/dev/null; then - exec python3 "${DIR}/openutv-supercharge-ffmpeg.py" "$@" + exec python3 "${SCRIPT}" "$@" else - exec python "${DIR}/openutv-supercharge-ffmpeg.py" "$@" + exec python "${SCRIPT}" "$@" fi diff --git a/src/bin/nsapps/UTV/py-interp.cmd b/src/bin/nsapps/UTV/py-interp.cmd new file mode 100644 index 00000000..69612965 --- /dev/null +++ b/src/bin/nsapps/UTV/py-interp.cmd @@ -0,0 +1,61 @@ +@echo off +setlocal + +:: Discover OpenUTVDeps runtime root directory +set "DEPS_ROOT=" +if defined UTV_DEPS_ROOT if exist "%UTV_DEPS_ROOT%\bin\OpenImageIO.dll" set "DEPS_ROOT=%UTV_DEPS_ROOT%" +if not defined DEPS_ROOT if defined OPENUTV_DEPS_ROOT if exist "%OPENUTV_DEPS_ROOT%\bin\OpenImageIO.dll" set "DEPS_ROOT=%OPENUTV_DEPS_ROOT%" + +if not defined DEPS_ROOT ( + for /f "tokens=2*" %%a in ('reg query "HKCU\Environment" /v "UTV_DEPS_ROOT" 2^>nul') do if exist "%%b\bin\OpenImageIO.dll" set "DEPS_ROOT=%%b" +) +if not defined DEPS_ROOT ( + for /f "tokens=2*" %%a in ('reg query "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment" /v "UTV_DEPS_ROOT" 2^>nul') do if exist "%%b\bin\OpenImageIO.dll" set "DEPS_ROOT=%%b" +) +if not defined DEPS_ROOT ( + for /f "tokens=2*" %%a in ('reg query "HKCU\Environment" /v "OPENUTV_DEPS_ROOT" 2^>nul') do if exist "%%b\bin\OpenImageIO.dll" set "DEPS_ROOT=%%b" +) +if not defined DEPS_ROOT ( + for /f "tokens=2*" %%a in ('reg query "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment" /v "OPENUTV_DEPS_ROOT" 2^>nul') do if exist "%%b\bin\OpenImageIO.dll" set "DEPS_ROOT=%%b" +) +if not defined DEPS_ROOT ( + for /f "tokens=2*" %%a in ('reg query "HKCU\Software\OpenUTV" /v "DepsPath" 2^>nul') do if exist "%%b\bin\OpenImageIO.dll" set "DEPS_ROOT=%%b" +) +if not defined DEPS_ROOT ( + for /f "tokens=2*" %%a in ('reg query "HKLM\Software\OpenUTV" /v "DepsPath" 2^>nul') do if exist "%%b\bin\OpenImageIO.dll" set "DEPS_ROOT=%%b" +) +if not defined DEPS_ROOT ( + for /d %%d in ("%ProgramFiles%\OpenUTVDeps*") do if exist "%%d\bin\OpenImageIO.dll" set "DEPS_ROOT=%%d" +) +if not defined DEPS_ROOT ( + for /d %%d in ("%LOCALAPPDATA%\Programs\OpenUTVDeps*") do if exist "%%d\bin\OpenImageIO.dll" set "DEPS_ROOT=%%d" +) + +if not defined DEPS_ROOT ( + echo OpenUTV: Could not locate OpenUTVDeps runtime dependencies. >&2 + exit /b 1 +) + +set "APP_DIR=%~dp0" +if "%APP_DIR:~-1%"=="\" set "APP_DIR=%APP_DIR:~0,-1%" + +set "PATH=%APP_DIR%;%DEPS_ROOT%\bin;%DEPS_ROOT%\tools\python3\Lib\site-packages\PySide6;%DEPS_ROOT%\tools\python3;%DEPS_ROOT%\tools\python3\Scripts;%PATH%" +set "PYTHONHOME=%DEPS_ROOT%\tools\python3" +set "QT_PLUGIN_PATH=%DEPS_ROOT%\tools\python3\Lib\site-packages\PySide6\plugins" +set "QT_QPA_PLATFORM_PLUGIN_PATH=%DEPS_ROOT%\tools\python3\Lib\site-packages\PySide6\plugins\platforms" +set "UTV_DEPS_ROOT=%DEPS_ROOT%" +set "OPENUTV_DEPS_ROOT=%DEPS_ROOT%" +set "UTV_HOME=%APP_DIR%" +set "OPENUTV_HOME=%APP_DIR%" + +if exist "%APP_DIR%\py-interp-bin.exe" ( + "%APP_DIR%\py-interp-bin.exe" %* +) else if exist "%APP_DIR%\py-interp.exe" ( + "%APP_DIR%\py-interp.exe" %* +) else if exist "%DEPS_ROOT%\tools\python3\python.exe" ( + "%DEPS_ROOT%\tools\python3\python.exe" %* +) else ( + echo OpenUTV: Could not locate py-interp or Python executable. >&2 + exit /b 1 +) +exit /b %ERRORLEVEL% diff --git a/src/bin/nsapps/UTV/utvio.cmd b/src/bin/nsapps/UTV/utvio.cmd new file mode 100644 index 00000000..1184388c --- /dev/null +++ b/src/bin/nsapps/UTV/utvio.cmd @@ -0,0 +1,63 @@ +@echo off +setlocal + +:: Discover OpenUTVDeps runtime root directory +set "DEPS_ROOT=" +if defined UTV_DEPS_ROOT if exist "%UTV_DEPS_ROOT%\bin\OpenImageIO.dll" set "DEPS_ROOT=%UTV_DEPS_ROOT%" +if not defined DEPS_ROOT if defined OPENUTV_DEPS_ROOT if exist "%OPENUTV_DEPS_ROOT%\bin\OpenImageIO.dll" set "DEPS_ROOT=%OPENUTV_DEPS_ROOT%" + +if not defined DEPS_ROOT ( + for /f "tokens=2*" %%a in ('reg query "HKCU\Environment" /v "UTV_DEPS_ROOT" 2^>nul') do if exist "%%b\bin\OpenImageIO.dll" set "DEPS_ROOT=%%b" +) +if not defined DEPS_ROOT ( + for /f "tokens=2*" %%a in ('reg query "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment" /v "UTV_DEPS_ROOT" 2^>nul') do if exist "%%b\bin\OpenImageIO.dll" set "DEPS_ROOT=%%b" +) +if not defined DEPS_ROOT ( + for /f "tokens=2*" %%a in ('reg query "HKCU\Environment" /v "OPENUTV_DEPS_ROOT" 2^>nul') do if exist "%%b\bin\OpenImageIO.dll" set "DEPS_ROOT=%%b" +) +if not defined DEPS_ROOT ( + for /f "tokens=2*" %%a in ('reg query "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment" /v "OPENUTV_DEPS_ROOT" 2^>nul') do if exist "%%b\bin\OpenImageIO.dll" set "DEPS_ROOT=%%b" +) +if not defined DEPS_ROOT ( + for /f "tokens=2*" %%a in ('reg query "HKCU\Software\OpenUTV" /v "DepsPath" 2^>nul') do if exist "%%b\bin\OpenImageIO.dll" set "DEPS_ROOT=%%b" +) +if not defined DEPS_ROOT ( + for /f "tokens=2*" %%a in ('reg query "HKLM\Software\OpenUTV" /v "DepsPath" 2^>nul') do if exist "%%b\bin\OpenImageIO.dll" set "DEPS_ROOT=%%b" +) +if not defined DEPS_ROOT ( + for /d %%d in ("%ProgramFiles%\OpenUTVDeps*") do if exist "%%d\bin\OpenImageIO.dll" set "DEPS_ROOT=%%d" +) +if not defined DEPS_ROOT ( + for /d %%d in ("%LOCALAPPDATA%\Programs\OpenUTVDeps*") do if exist "%%d\bin\OpenImageIO.dll" set "DEPS_ROOT=%%d" +) + +if not defined DEPS_ROOT ( + echo OpenUTV: Could not locate OpenUTVDeps runtime dependencies. >&2 + exit /b 1 +) + +set "APP_DIR=%~dp0" +if "%APP_DIR:~-1%"=="\" set "APP_DIR=%APP_DIR:~0,-1%" + +set "PATH=%APP_DIR%;%DEPS_ROOT%\bin;%DEPS_ROOT%\tools\python3\Lib\site-packages\PySide6;%DEPS_ROOT%\tools\python3;%DEPS_ROOT%\tools\python3\Scripts;%PATH%" +set "PYTHONHOME=%DEPS_ROOT%\tools\python3" +set "QT_PLUGIN_PATH=%DEPS_ROOT%\tools\python3\Lib\site-packages\PySide6\plugins" +set "QT_QPA_PLATFORM_PLUGIN_PATH=%DEPS_ROOT%\tools\python3\Lib\site-packages\PySide6\plugins\platforms" +set "UTV_DEPS_ROOT=%DEPS_ROOT%" +set "OPENUTV_DEPS_ROOT=%DEPS_ROOT%" +set "UTV_HOME=%APP_DIR%" +set "OPENUTV_HOME=%APP_DIR%" + +if exist "%APP_DIR%\utvio-bin.exe" ( + "%APP_DIR%\utvio-bin.exe" %* +) else if exist "%APP_DIR%\rvio-bin.exe" ( + "%APP_DIR%\rvio-bin.exe" %* +) else if exist "%APP_DIR%\utvio.exe" ( + "%APP_DIR%\utvio.exe" %* +) else if exist "%APP_DIR%\rvio.exe" ( + "%APP_DIR%\rvio.exe" %* +) else ( + echo OpenUTV: Could not locate utvio executable in %APP_DIR%. >&2 + exit /b 1 +) +exit /b %ERRORLEVEL% diff --git a/src/bin/nsapps/UTV/utvls.cmd b/src/bin/nsapps/UTV/utvls.cmd new file mode 100644 index 00000000..168941aa --- /dev/null +++ b/src/bin/nsapps/UTV/utvls.cmd @@ -0,0 +1,63 @@ +@echo off +setlocal + +:: Discover OpenUTVDeps runtime root directory +set "DEPS_ROOT=" +if defined UTV_DEPS_ROOT if exist "%UTV_DEPS_ROOT%\bin\OpenImageIO.dll" set "DEPS_ROOT=%UTV_DEPS_ROOT%" +if not defined DEPS_ROOT if defined OPENUTV_DEPS_ROOT if exist "%OPENUTV_DEPS_ROOT%\bin\OpenImageIO.dll" set "DEPS_ROOT=%OPENUTV_DEPS_ROOT%" + +if not defined DEPS_ROOT ( + for /f "tokens=2*" %%a in ('reg query "HKCU\Environment" /v "UTV_DEPS_ROOT" 2^>nul') do if exist "%%b\bin\OpenImageIO.dll" set "DEPS_ROOT=%%b" +) +if not defined DEPS_ROOT ( + for /f "tokens=2*" %%a in ('reg query "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment" /v "UTV_DEPS_ROOT" 2^>nul') do if exist "%%b\bin\OpenImageIO.dll" set "DEPS_ROOT=%%b" +) +if not defined DEPS_ROOT ( + for /f "tokens=2*" %%a in ('reg query "HKCU\Environment" /v "OPENUTV_DEPS_ROOT" 2^>nul') do if exist "%%b\bin\OpenImageIO.dll" set "DEPS_ROOT=%%b" +) +if not defined DEPS_ROOT ( + for /f "tokens=2*" %%a in ('reg query "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment" /v "OPENUTV_DEPS_ROOT" 2^>nul') do if exist "%%b\bin\OpenImageIO.dll" set "DEPS_ROOT=%%b" +) +if not defined DEPS_ROOT ( + for /f "tokens=2*" %%a in ('reg query "HKCU\Software\OpenUTV" /v "DepsPath" 2^>nul') do if exist "%%b\bin\OpenImageIO.dll" set "DEPS_ROOT=%%b" +) +if not defined DEPS_ROOT ( + for /f "tokens=2*" %%a in ('reg query "HKLM\Software\OpenUTV" /v "DepsPath" 2^>nul') do if exist "%%b\bin\OpenImageIO.dll" set "DEPS_ROOT=%%b" +) +if not defined DEPS_ROOT ( + for /d %%d in ("%ProgramFiles%\OpenUTVDeps*") do if exist "%%d\bin\OpenImageIO.dll" set "DEPS_ROOT=%%d" +) +if not defined DEPS_ROOT ( + for /d %%d in ("%LOCALAPPDATA%\Programs\OpenUTVDeps*") do if exist "%%d\bin\OpenImageIO.dll" set "DEPS_ROOT=%%d" +) + +if not defined DEPS_ROOT ( + echo OpenUTV: Could not locate OpenUTVDeps runtime dependencies. >&2 + exit /b 1 +) + +set "APP_DIR=%~dp0" +if "%APP_DIR:~-1%"=="\" set "APP_DIR=%APP_DIR:~0,-1%" + +set "PATH=%APP_DIR%;%DEPS_ROOT%\bin;%DEPS_ROOT%\tools\python3\Lib\site-packages\PySide6;%DEPS_ROOT%\tools\python3;%DEPS_ROOT%\tools\python3\Scripts;%PATH%" +set "PYTHONHOME=%DEPS_ROOT%\tools\python3" +set "QT_PLUGIN_PATH=%DEPS_ROOT%\tools\python3\Lib\site-packages\PySide6\plugins" +set "QT_QPA_PLATFORM_PLUGIN_PATH=%DEPS_ROOT%\tools\python3\Lib\site-packages\PySide6\plugins\platforms" +set "UTV_DEPS_ROOT=%DEPS_ROOT%" +set "OPENUTV_DEPS_ROOT=%DEPS_ROOT%" +set "UTV_HOME=%APP_DIR%" +set "OPENUTV_HOME=%APP_DIR%" + +if exist "%APP_DIR%\utvls-bin.exe" ( + "%APP_DIR%\utvls-bin.exe" %* +) else if exist "%APP_DIR%\rvls-bin.exe" ( + "%APP_DIR%\rvls-bin.exe" %* +) else if exist "%APP_DIR%\utvls.exe" ( + "%APP_DIR%\utvls.exe" %* +) else if exist "%APP_DIR%\rvls.exe" ( + "%APP_DIR%\rvls.exe" %* +) else ( + echo OpenUTV: Could not locate utvls executable in %APP_DIR%. >&2 + exit /b 1 +) +exit /b %ERRORLEVEL% diff --git a/src/bin/nsapps/UTV/utvpkg.cmd b/src/bin/nsapps/UTV/utvpkg.cmd new file mode 100644 index 00000000..a5c522f3 --- /dev/null +++ b/src/bin/nsapps/UTV/utvpkg.cmd @@ -0,0 +1,63 @@ +@echo off +setlocal + +:: Discover OpenUTVDeps runtime root directory +set "DEPS_ROOT=" +if defined UTV_DEPS_ROOT if exist "%UTV_DEPS_ROOT%\bin\OpenImageIO.dll" set "DEPS_ROOT=%UTV_DEPS_ROOT%" +if not defined DEPS_ROOT if defined OPENUTV_DEPS_ROOT if exist "%OPENUTV_DEPS_ROOT%\bin\OpenImageIO.dll" set "DEPS_ROOT=%OPENUTV_DEPS_ROOT%" + +if not defined DEPS_ROOT ( + for /f "tokens=2*" %%a in ('reg query "HKCU\Environment" /v "UTV_DEPS_ROOT" 2^>nul') do if exist "%%b\bin\OpenImageIO.dll" set "DEPS_ROOT=%%b" +) +if not defined DEPS_ROOT ( + for /f "tokens=2*" %%a in ('reg query "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment" /v "UTV_DEPS_ROOT" 2^>nul') do if exist "%%b\bin\OpenImageIO.dll" set "DEPS_ROOT=%%b" +) +if not defined DEPS_ROOT ( + for /f "tokens=2*" %%a in ('reg query "HKCU\Environment" /v "OPENUTV_DEPS_ROOT" 2^>nul') do if exist "%%b\bin\OpenImageIO.dll" set "DEPS_ROOT=%%b" +) +if not defined DEPS_ROOT ( + for /f "tokens=2*" %%a in ('reg query "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment" /v "OPENUTV_DEPS_ROOT" 2^>nul') do if exist "%%b\bin\OpenImageIO.dll" set "DEPS_ROOT=%%b" +) +if not defined DEPS_ROOT ( + for /f "tokens=2*" %%a in ('reg query "HKCU\Software\OpenUTV" /v "DepsPath" 2^>nul') do if exist "%%b\bin\OpenImageIO.dll" set "DEPS_ROOT=%%b" +) +if not defined DEPS_ROOT ( + for /f "tokens=2*" %%a in ('reg query "HKLM\Software\OpenUTV" /v "DepsPath" 2^>nul') do if exist "%%b\bin\OpenImageIO.dll" set "DEPS_ROOT=%%b" +) +if not defined DEPS_ROOT ( + for /d %%d in ("%ProgramFiles%\OpenUTVDeps*") do if exist "%%d\bin\OpenImageIO.dll" set "DEPS_ROOT=%%d" +) +if not defined DEPS_ROOT ( + for /d %%d in ("%LOCALAPPDATA%\Programs\OpenUTVDeps*") do if exist "%%d\bin\OpenImageIO.dll" set "DEPS_ROOT=%%d" +) + +if not defined DEPS_ROOT ( + echo OpenUTV: Could not locate OpenUTVDeps runtime dependencies. >&2 + exit /b 1 +) + +set "APP_DIR=%~dp0" +if "%APP_DIR:~-1%"=="\" set "APP_DIR=%APP_DIR:~0,-1%" + +set "PATH=%APP_DIR%;%DEPS_ROOT%\bin;%DEPS_ROOT%\tools\python3\Lib\site-packages\PySide6;%DEPS_ROOT%\tools\python3;%DEPS_ROOT%\tools\python3\Scripts;%PATH%" +set "PYTHONHOME=%DEPS_ROOT%\tools\python3" +set "QT_PLUGIN_PATH=%DEPS_ROOT%\tools\python3\Lib\site-packages\PySide6\plugins" +set "QT_QPA_PLATFORM_PLUGIN_PATH=%DEPS_ROOT%\tools\python3\Lib\site-packages\PySide6\plugins\platforms" +set "UTV_DEPS_ROOT=%DEPS_ROOT%" +set "OPENUTV_DEPS_ROOT=%DEPS_ROOT%" +set "UTV_HOME=%APP_DIR%" +set "OPENUTV_HOME=%APP_DIR%" + +if exist "%APP_DIR%\utvpkg-bin.exe" ( + "%APP_DIR%\utvpkg-bin.exe" %* +) else if exist "%APP_DIR%\rvpkg-bin.exe" ( + "%APP_DIR%\rvpkg-bin.exe" %* +) else if exist "%APP_DIR%\utvpkg.exe" ( + "%APP_DIR%\utvpkg.exe" %* +) else if exist "%APP_DIR%\rvpkg.exe" ( + "%APP_DIR%\rvpkg.exe" %* +) else ( + echo OpenUTV: Could not locate utvpkg executable in %APP_DIR%. >&2 + exit /b 1 +) +exit /b %ERRORLEVEL% diff --git a/src/lib/app/QTBundle/QTBundle.cpp b/src/lib/app/QTBundle/QTBundle.cpp index ba40650e..5b1d8cb2 100644 --- a/src/lib/app/QTBundle/QTBundle.cpp +++ b/src/lib/app/QTBundle/QTBundle.cpp @@ -89,7 +89,11 @@ namespace TwkApp m_pyhome.cd("python" PYTHON_VERSION); bool setPythonHome = !(getenv("PYTHONHOME") && getenv("RV_ALLOW_SITE_PYTHONHOME")); QString discoveredDepsRoot; - const char* depsRoot = getenv("OPENUTV_DEPS_ROOT"); + const char* depsRoot = getenv("UTV_DEPS_ROOT"); + if (!depsRoot) + { + depsRoot = getenv("OPENUTV_DEPS_ROOT"); + } #ifdef PLATFORM_WINDOWS if (!depsRoot) { diff --git a/src/lib/app/RvCommon/RvKeybindingsManager.cpp b/src/lib/app/RvCommon/RvKeybindingsManager.cpp index 7c7f4f08..1cef4a92 100644 --- a/src/lib/app/RvCommon/RvKeybindingsManager.cpp +++ b/src/lib/app/RvCommon/RvKeybindingsManager.cpp @@ -37,8 +37,11 @@ namespace Rv QString RvKeybindingsManager::defaultKeybindingsPath() const { - // 1. Environment variable override - const char* envPath = getenv("OPENUTV_KEYBINDINGS_FILE"); + const char* envPath = getenv("UTV_KEYBINDINGS_FILE"); + if (!envPath) + { + envPath = getenv("OPENUTV_KEYBINDINGS_FILE"); + } if (!envPath) { envPath = getenv("RV_KEYBINDINGS_FILE"); diff --git a/src/lib/app/RvCommon/RvPreferences.cpp b/src/lib/app/RvCommon/RvPreferences.cpp index abd25e5f..1c33aa1a 100644 --- a/src/lib/app/RvCommon/RvPreferences.cpp +++ b/src/lib/app/RvCommon/RvPreferences.cpp @@ -445,9 +445,9 @@ namespace Rv s.setNum(opts.readerThreads); m_ui.rthreadEdit->setText(s); - QString hwEnv = QString::fromLocal8Bit(qgetenv("OPENUTV_HWACCEL")); + QString hwEnv = QString::fromLocal8Bit(qgetenv("UTV_HWACCEL")); if (hwEnv.isEmpty()) - hwEnv = QString::fromLocal8Bit(qgetenv("UTV_HWACCEL")); + hwEnv = QString::fromLocal8Bit(qgetenv("OPENUTV_HWACCEL")); if (hwEnv.isEmpty()) hwEnv = QString::fromLocal8Bit(qgetenv("RV_HWACCEL")); if (!hwEnv.isEmpty()) @@ -965,15 +965,12 @@ namespace Rv opts.networkHost = (char*)((opts.networkHostBuf.empty()) ? 0 : opts.networkHostBuf.c_str()); opts.readerThreads = settings.value("readerThreads", opts.readerThreads).toInt(); opts.hwDecodeMode = settings.value("hardwareDecodeMode", opts.hwDecodeMode).toInt(); - if (qEnvironmentVariableIsEmpty("OPENUTV_HWACCEL") && qEnvironmentVariableIsEmpty("UTV_HWACCEL") + if (qEnvironmentVariableIsEmpty("UTV_HWACCEL") && qEnvironmentVariableIsEmpty("OPENUTV_HWACCEL") && qEnvironmentVariableIsEmpty("RV_HWACCEL")) { - if (opts.hwDecodeMode == 2) - qputenv("OPENUTV_HWACCEL", "none"); - else if (opts.hwDecodeMode == 1) - qputenv("OPENUTV_HWACCEL", "prores"); - else - qputenv("OPENUTV_HWACCEL", "all"); + const char* val = (opts.hwDecodeMode == 2) ? "none" : ((opts.hwDecodeMode == 1) ? "prores" : "all"); + qputenv("UTV_HWACCEL", val); + qputenv("OPENUTV_HWACCEL", val); } opts.autoRetime = int(settings.value("autoRetime", opts.autoRetime ? true : false).toBool()); opts.autoSetupACES = int(settings.value("autoSetupACES", opts.autoSetupACES ? true : false).toBool()); @@ -1323,12 +1320,9 @@ namespace Rv settings.setValue("readerThreads", m_ui.rthreadEdit->text().toInt()); int hwMode = m_ui.hardwareDecodeCombo->currentIndex(); settings.setValue("hardwareDecodeMode", hwMode); - if (hwMode == 2) - qputenv("OPENUTV_HWACCEL", "none"); - else if (hwMode == 1) - qputenv("OPENUTV_HWACCEL", "prores"); - else - qputenv("OPENUTV_HWACCEL", "all"); + const char* val = (hwMode == 2) ? "none" : ((hwMode == 1) ? "prores" : "all"); + qputenv("UTV_HWACCEL", val); + qputenv("OPENUTV_HWACCEL", val); settings.setValue("autoRetime", m_ui.autoRetimeToggle->checkState() == Qt::Checked); settings.setValue("useCrashReporter", m_ui.useCrashReporterToggle->checkState() == Qt::Checked); settings.setValue("fontSize1", m_ui.fontSizeSpinBox->value()); diff --git a/src/lib/image/MovieFFMpeg/MovieFFMpeg.cpp b/src/lib/image/MovieFFMpeg/MovieFFMpeg.cpp index e7f31e22..3ca85f7a 100644 --- a/src/lib/image/MovieFFMpeg/MovieFFMpeg.cpp +++ b/src/lib/image/MovieFFMpeg/MovieFFMpeg.cpp @@ -1074,9 +1074,9 @@ namespace TwkMovie HwDecodeMode getHardwareDecodeMode() { - const char* env = getenv("OPENUTV_HWACCEL"); + const char* env = getenv("UTV_HWACCEL"); if (!env) - env = getenv("UTV_HWACCEL"); + env = getenv("OPENUTV_HWACCEL"); if (!env) env = getenv("RV_HWACCEL"); @@ -1120,9 +1120,9 @@ namespace TwkMovie { std::vector candidates; - const char* env = getenv("OPENUTV_HWACCEL"); + const char* env = getenv("UTV_HWACCEL"); if (!env) - env = getenv("UTV_HWACCEL"); + env = getenv("OPENUTV_HWACCEL"); if (!env) env = getenv("RV_HWACCEL"); From a1259f8d0128f6b385ef3dec5323e89666c380c5 Mon Sep 17 00:00:00 2001 From: Michael Oliver Date: Tue, 29 Sep 2026 15:31:44 -0700 Subject: [PATCH 3/5] fix(oiio): fix IOoiio write compile errors and only advertise writable formats - Replace non-existent APIs in IOoiio::writeImage: FrameBuffer::pixelAspect() -> pixelAspectRatio(), FrameBuffer::data() -> pixels(), ImageSpec::pixelaspect -> "PixelAspectRatio" attribute. - Query OIIO's output_format_list / extension map at plugin construction and only register Write for extensions that have an OIIO writer in this build. Homebrew's OIIO has no QOI or DDS writer, so these are now Read-only and utvio reports "No plugins support (write) format" instead of failing inside the OIIO plugin. - Correct plugin identifiers in the native-writer comment. Refs #59 Co-Authored-By: Claude Opus 5.5 Signed-off-by: Michael Oliver --- src/lib/image/IOoiio/IOoiio.cpp | 77 +++++++++++++++++++++------------ 1 file changed, 50 insertions(+), 27 deletions(-) diff --git a/src/lib/image/IOoiio/IOoiio.cpp b/src/lib/image/IOoiio/IOoiio.cpp index a9cba900..5ed2b31c 100644 --- a/src/lib/image/IOoiio/IOoiio.cpp +++ b/src/lib/image/IOoiio/IOoiio.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include @@ -32,10 +33,32 @@ namespace TwkFB StringPairVector codecs; unsigned int r = ImageRead; unsigned int w = ImageWrite; - unsigned int rw = r | w; + + // + // Only advertise Write for extensions whose OIIO format actually has an + // output plugin in this OIIO build (e.g. Homebrew's OIIO has no QOI + // writer). Otherwise GenericIO would route writes here and fail. + // + set writableExts; + { + set outputFormats; + for (const auto& name : Strutil::splits(get_string_attribute("output_format_list"), ",")) + outputFormats.insert(name); + + for (const auto& entry : get_extension_map()) + { + if (outputFormats.count(entry.first)) + { + for (const auto& ext : entry.second) + writableExts.insert(Strutil::lower(ext)); + } + } + } + + auto rw = [&](const char* ext) { return writableExts.count(ext) ? (r | w) : r; }; // Dedicated native writer plugins handle the following formats with custom streaming pipelines: - // IOpng ("m9"), IOdpx ("m5"), IOcin ("m4"), IOjpeg ("m2"), IOtarga ("m6"), IOrla ("m7"), IOrgbe ("m8"). + // IOpng ("m9"), IOdpx ("m5"), IOcin ("z_cin"), IOjpeg ("m2"), IOtarga ("z_targa"), IOrla ("z_rla"), IOrgbe ("z_rgbe"). // These MUST be registered as Read-only in IOoiio so write requests route directly to their native writers. addType("png", "Portable Network Graphics Image", r, codecs); addType("dpx", "SMPTE DPX", r, codecs); @@ -59,27 +82,27 @@ namespace TwkFB addType("z", "Pixar Z-Depth", r, codecs); // Formats written via OpenImageIO (no dedicated native writer plugin) - addType("webp", "Google WebP", rw, codecs); - addType("qoi", "Quite OK Image", rw, codecs); - addType("bmp", "Windows Bitmap", rw, codecs); - addType("sgi", "SGI image", rw, codecs); - addType("bw", "SGI image", rw, codecs); - addType("rgb", "SGI image", rw, codecs); - addType("rgba", "SGI image", rw, codecs); - addType("inta", "SGI image", rw, codecs); - addType("int", "SGI image", rw, codecs); - addType("pnm", "PNM", rw, codecs); - addType("pbm", "Portable Network Graphics", rw, codecs); - addType("pgm", "Portable Network Graphics", rw, codecs); - addType("ppm", "Portable Network Grapics", rw, codecs); - addType("fits", "FITS", rw, codecs); - addType("iff", "IFF", rw, codecs); - addType("dds", "Direct Draw Surface", rw, codecs); - addType("heic", "High Efficiency Image File", rw, codecs); - addType("heif", "High Efficiency Image File", rw, codecs); - addType("hif", "High Efficiency Image File", rw, codecs); - addType("avif", "AV1 Image File", rw, codecs); - addType("jxl", "JPEG XL Image", rw, codecs); + addType("webp", "Google WebP", rw("webp"), codecs); + addType("qoi", "Quite OK Image", rw("qoi"), codecs); + addType("bmp", "Windows Bitmap", rw("bmp"), codecs); + addType("sgi", "SGI image", rw("sgi"), codecs); + addType("bw", "SGI image", rw("bw"), codecs); + addType("rgb", "SGI image", rw("rgb"), codecs); + addType("rgba", "SGI image", rw("rgba"), codecs); + addType("inta", "SGI image", rw("inta"), codecs); + addType("int", "SGI image", rw("int"), codecs); + addType("pnm", "PNM", rw("pnm"), codecs); + addType("pbm", "Portable Network Graphics", rw("pbm"), codecs); + addType("pgm", "Portable Network Graphics", rw("pgm"), codecs); + addType("ppm", "Portable Network Grapics", rw("ppm"), codecs); + addType("fits", "FITS", rw("fits"), codecs); + addType("iff", "IFF", rw("iff"), codecs); + addType("dds", "Direct Draw Surface", rw("dds"), codecs); + addType("heic", "High Efficiency Image File", rw("heic"), codecs); + addType("heif", "High Efficiency Image File", rw("heif"), codecs); + addType("hif", "High Efficiency Image File", rw("hif"), codecs); + addType("avif", "AV1 Image File", rw("avif"), codecs); + addType("jxl", "JPEG XL Image", rw("jxl"), codecs); // These are handled by their dedicated optimized streaming plugins: // IOexr ("m0"), IOtiff ("m1"), IOjpeg ("m2"), IOhtj2k ("m7") @@ -586,11 +609,11 @@ namespace TwkFB if (request.pixelAspect != 1.0f && request.pixelAspect != 0.0f) { - spec.pixelaspect = request.pixelAspect; + spec.attribute("PixelAspectRatio", request.pixelAspect); } - else if (outfb->pixelAspect() != 1.0f && outfb->pixelAspect() != 0.0f) + else if (outfb->pixelAspectRatio() != 1.0f && outfb->pixelAspectRatio() != 0.0f) { - spec.pixelaspect = outfb->pixelAspect(); + spec.attribute("PixelAspectRatio", outfb->pixelAspectRatio()); } if (!request.compression.empty()) @@ -616,7 +639,7 @@ namespace TwkFB stride_t xstride = outfb->pixelSize(); stride_t ystride = outfb->scanlinePaddedSize(); - if (!out->write_image(format, outfb->data(), xstride, ystride)) + if (!out->write_image(format, outfb->pixels(), xstride, ystride)) { TWK_THROW_STREAM(IOException, "OIIO: Error writing \"" << filename << "\": " << out->geterror()); } From f2b080a569f08295adabaf4af0e8a16fd263cfdc Mon Sep 17 00:00:00 2001 From: Michael Oliver Date: Tue, 29 Sep 2026 15:36:57 -0700 Subject: [PATCH 4/5] build: regenerate formats.gto caches when format plugins are relinked formats.gto and movieformats.gto were generated by custom commands that only depended on the image_formats/movie_formats utility targets. Utility target dependencies order the build but do not trigger re-runs, so an incremental build kept stale plugin capabilities after a plugin changed, and GenericIO routed reads/writes based on outdated information. rv_stage now records each IMAGE_FORMAT/MOVIE_FORMAT plugin's output file on the aggregate target, and the cache generators depend on those files. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Michael Oliver --- cmake/macros/rv_stage.cmake | 12 ++++++++++++ src/bin/imgtools/makeFBIOformats/CMakeLists.txt | 2 +- src/bin/imgtools/makeMovieIOformats/CMakeLists.txt | 2 +- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/cmake/macros/rv_stage.cmake b/cmake/macros/rv_stage.cmake index 61bd10f0..5b5226a7 100644 --- a/cmake/macros/rv_stage.cmake +++ b/cmake/macros/rv_stage.cmake @@ -504,6 +504,12 @@ FUNCTION(rv_stage) ENDIF() ADD_DEPENDENCIES(image_formats ${arg_TARGET}) + # Track the plugin file so the image formats .gto cache is regenerated when any plugin is relinked + SET_PROPERTY( + TARGET image_formats + APPEND + PROPERTY RV_FORMAT_PLUGIN_FILES "$" + ) ADD_SHARED_LIBRARY_LIST(${arg_TARGET}) @@ -533,6 +539,12 @@ FUNCTION(rv_stage) ENDIF() ADD_DEPENDENCIES(movie_formats ${arg_TARGET}) + # Track the plugin file so the movie formats .gto cache is regenerated when any plugin is relinked + SET_PROPERTY( + TARGET movie_formats + APPEND + PROPERTY RV_FORMAT_PLUGIN_FILES "$" + ) ADD_SHARED_LIBRARY_LIST(${arg_TARGET}) diff --git a/src/bin/imgtools/makeFBIOformats/CMakeLists.txt b/src/bin/imgtools/makeFBIOformats/CMakeLists.txt index ea14819d..9d2f6a52 100644 --- a/src/bin/imgtools/makeFBIOformats/CMakeLists.txt +++ b/src/bin/imgtools/makeFBIOformats/CMakeLists.txt @@ -34,7 +34,7 @@ ADD_CUSTOM_COMMAND( OUTPUT ${RV_STAGE_PLUGINS_IMAGEFORMATS_DIR}/formats.gto COMMAND $ ${RV_STAGE_PLUGINS_IMAGEFORMATS_DIR} WORKING_DIRECTORY ${RV_STAGE_BIN_DIR} - DEPENDS ${_target} image_formats + DEPENDS ${_target} image_formats "$>" ) ADD_CUSTOM_TARGET( diff --git a/src/bin/imgtools/makeMovieIOformats/CMakeLists.txt b/src/bin/imgtools/makeMovieIOformats/CMakeLists.txt index a719f4a5..2f6ba1be 100644 --- a/src/bin/imgtools/makeMovieIOformats/CMakeLists.txt +++ b/src/bin/imgtools/makeMovieIOformats/CMakeLists.txt @@ -52,7 +52,7 @@ ADD_CUSTOM_COMMAND( OUTPUT ${RV_STAGE_PLUGINS_MOVIEFORMATS_DIR}/movieformats.gto COMMAND $ ${RV_STAGE_PLUGINS_MOVIEFORMATS_DIR} WORKING_DIRECTORY ${RV_STAGE_BIN_DIR} - DEPENDS ${_target} movie_formats + DEPENDS ${_target} movie_formats "$>" ) ADD_CUSTOM_TARGET( From 07c00933a7e8f6befee385843d6503ad29184405 Mon Sep 17 00:00:00 2001 From: Michael Oliver Date: Tue, 29 Sep 2026 15:37:04 -0700 Subject: [PATCH 5/5] fix(oiio): harden IOoiio writer for alpha, PNM, FITS and error paths - Drop a trailing alpha channel when the OIIO writer does not support alpha (PNM), instead of failing with "pnm does not support 4-channel". - Write integer samples for PNM: OIIO's PNM writer emits bottom-to-top PFM for any floating point spec regardless of extension, which produced vertically flipped .ppm/.pgm files from float frame buffers. - Promote HALF to FLOAT for FITS: OIIO writes BITPIX=-32 for HALF but still writes 16-bit samples, producing a truncated, unreadable file (still present on OIIO main). - Install the temporary FrameBuffer cleanup guard before any conversion so intermediates are not leaked if a conversion throws. - Check ImageOutput::close() and report its error. Refs #59 Co-Authored-By: Claude Opus 5.5 Signed-off-by: Michael Oliver --- src/lib/image/IOoiio/IOoiio.cpp | 76 ++++++++++++++++++++++++--------- 1 file changed, 55 insertions(+), 21 deletions(-) diff --git a/src/lib/image/IOoiio/IOoiio.cpp b/src/lib/image/IOoiio/IOoiio.cpp index 5ed2b31c..ee68282b 100644 --- a/src/lib/image/IOoiio/IOoiio.cpp +++ b/src/lib/image/IOoiio/IOoiio.cpp @@ -473,6 +473,19 @@ namespace TwkFB { const FrameBuffer* outfb = &img; + // Ensure temporary FrameBuffer is cleanly deleted upon return/exception + struct FBAutoCleanup + { + const FrameBuffer*& current; + const FrameBuffer& original; + + ~FBAutoCleanup() + { + if (current != &original) + delete current; + } + } autoCleanup{outfb, img}; + // // Merge planar frames if needed // @@ -551,19 +564,6 @@ namespace TwkFB flip(const_cast(outfb)); } - // Ensure temporary FrameBuffer is cleanly deleted upon return/exception - struct FBAutoCleanup - { - const FrameBuffer*& current; - const FrameBuffer& original; - - ~FBAutoCleanup() - { - if (current != &original) - delete current; - } - } autoCleanup{outfb, img}; - std::unique_ptr out = ImageOutput::create(filename); if (!out) { @@ -596,15 +596,46 @@ namespace TwkFB break; } - ImageSpec spec(outfb->width(), outfb->height(), outfb->numChannels(), format); + // + // Drop a trailing alpha channel for writers that cannot store one + // (e.g. PNM). The pixel stride still spans all source channels, so + // OIIO simply skips the alpha values. + // + const int numChannels = outfb->numChannels(); + const bool hasChannelNames = outfb->channelNames().size() == static_cast(numChannels); + const bool lastIsAlpha = !hasChannelNames || outfb->channelNames().back() == "A"; + int nchannels = numChannels; - if (outfb->channelNames().size() == static_cast(outfb->numChannels())) + if ((numChannels == 2 || numChannels == 4) && lastIsAlpha && !out->supports("alpha")) { - spec.channelnames.clear(); - for (size_t i = 0; i < outfb->channelNames().size(); ++i) - { - spec.channelnames.push_back(outfb->channelNames()[i]); - } + nchannels = numChannels - 1; + } + + // + // Work around OIIO writers that mishandle some sample types. OIIO + // converts from the in-memory format to the file format. + // + // pnm: emits PFM (bottom-to-top float) for any floating point spec + // regardless of extension, so store integer samples for + // .ppm/.pgm/.pbm/.pnm. + // fits: writes BITPIX=-32 for HALF but still writes 16-bit samples, + // producing a truncated file, so promote HALF to FLOAT. + // + TypeDesc fileFormat = format; + if (Strutil::iequals(out->format_name(), "pnm") && format.is_floating_point()) + { + fileFormat = TypeDesc::UINT16; + } + else if (Strutil::iequals(out->format_name(), "fits") && format == TypeDesc::HALF) + { + fileFormat = TypeDesc::FLOAT; + } + + ImageSpec spec(outfb->width(), outfb->height(), nchannels, fileFormat); + + if (hasChannelNames) + { + spec.channelnames.assign(outfb->channelNames().begin(), outfb->channelNames().begin() + nchannels); } if (request.pixelAspect != 1.0f && request.pixelAspect != 0.0f) @@ -644,7 +675,10 @@ namespace TwkFB TWK_THROW_STREAM(IOException, "OIIO: Error writing \"" << filename << "\": " << out->geterror()); } - out->close(); + if (!out->close()) + { + TWK_THROW_STREAM(IOException, "OIIO: Error closing \"" << filename << "\": " << out->geterror()); + } } } // End namespace TwkFB