Skip to content

Commit 1d94069

Browse files
authored
fix(win): Embed manifest in OIIO .exes to enable long path handling (AcademySoftwareFoundation#5066)
Adds an embedded manifest to OIIO's generated executables on Windows, which enables "long path" awareness. This is done via a "resource" file, with the manifest resource ID specified. From what I've read, this is more broadly compatible with e.g. non-MSVC build toolchains like MinGW. The manifest file was created by building OIIO from `main`, extracting the manifest from the resulting `oiiotool` (using the `mt` tool), and manually adding in the XML snippet from https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=registry#application-manifest-updates-to-declare-long-path-capability. Fixes AcademySoftwareFoundation#5064 Added a new Windows-only test to validate that `oiiotool` can correctly read long image paths. The one executable that I have not tested is `iv`. The only reason I'm more curious about it than others is that it is a GUI executable, so I'm not sure if its default --------- Signed-off-by: Nathan Rusch <nrusch@users.noreply.github.com>
1 parent 7c4b306 commit 1d94069

File tree

6 files changed

+139
-2
lines changed

6 files changed

+139
-2
lines changed

src/cmake/fancy_add_executable.cmake

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,12 @@ macro (fancy_add_executable)
4444
check_is_enabled (${_target_NAME} _target_NAME_enabled)
4545
if (_target_NAME_enabled)
4646
add_executable (${_target_NAME} ${_target_SRC})
47+
if (WIN32)
48+
# Disable default manifest generation to avoid conflicts.
49+
target_link_options(${_target_NAME} PRIVATE "/MANIFEST:NO")
50+
# Include Windows resource file, which will in turn embed our exe manifest.
51+
target_sources(${_target_NAME} PRIVATE "${PROJECT_SOURCE_DIR}/src/windows/oiio_exe.rc")
52+
endif ()
4753
target_include_directories (${_target_NAME} PRIVATE ${_target_INCLUDE_DIRS})
4854
target_include_directories (${_target_NAME} SYSTEM PRIVATE ${_target_SYSTEM_INCLUDE_DIRS})
4955
target_compile_definitions (${_target_NAME} PRIVATE

src/cmake/testing.cmake

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -363,13 +363,31 @@ macro (oiio_add_all_tests)
363363
ENABLEVAR ENABLE_TARGA
364364
IMAGEDIR oiio-images)
365365
endif()
366-
if (NOT WIN32)
366+
if (WIN32)
367+
if (OIIO_BUILD_TOOLS)
368+
# Add test for long path handling if support is enabled at the system level.
369+
execute_process (
370+
COMMAND ${Python3_EXECUTABLE} "${CMAKE_SOURCE_DIR}/testsuite/windows-long-paths/check_registry.py"
371+
RESULT_VARIABLE _reg_check_status
372+
OUTPUT_QUIET ERROR_QUIET
373+
)
374+
if (_reg_check_status EQUAL 0)
375+
add_test (NAME windows-long-paths
376+
COMMAND
377+
${Python3_EXECUTABLE} "${CMAKE_SOURCE_DIR}/testsuite/windows-long-paths/test.py"
378+
--source-root "${CMAKE_SOURCE_DIR}"
379+
--oiiotool-path $<TARGET_FILE:oiiotool>)
380+
else ()
381+
message (STATUS "Skipping Windows long path test: System-level support is not enabled")
382+
endif ()
383+
endif ()
384+
else ()
367385
oiio_add_tests (term
368386
ENABLEVAR ENABLE_TERM)
369387
# I just could not get this test to work on Windows CI. The test fails
370388
# when comparing the output, but the saved artifacts compare just fine
371389
# on my system. Maybe someone will come back to this.
372-
endif ()
390+
endif ()
373391
oiio_add_tests (tiff-suite tiff-depths tiff-misc
374392
IMAGEDIR oiio-images/libtiffpic)
375393
oiio_add_tests (webp

src/windows/oiio_exe.manifest

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2+
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
3+
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
4+
<security>
5+
<requestedPrivileges>
6+
<requestedExecutionLevel level="asInvoker" uiAccess="false"></requestedExecutionLevel>
7+
</requestedPrivileges>
8+
</security>
9+
</trustInfo>
10+
<application xmlns="urn:schemas-microsoft-com:asm.v3">
11+
<windowsSettings xmlns:ws2="http://schemas.microsoft.com/SMI/2016/WindowsSettings">
12+
<ws2:longPathAware>true</ws2:longPathAware>
13+
</windowsSettings>
14+
</application>
15+
</assembly>

src/windows/oiio_exe.rc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// Resource script for OIIO EXEs.
2+
3+
// The value is invariant, so should be safe to hardcode, but we could replace
4+
// 24 with `RT_MANIFEST` by adding `#include "winres.h"` first.
5+
// Reference: https://learn.microsoft.com/en-us/windows/win32/menurc/user-defined-resource
6+
1 24 "oiio_exe.manifest"
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"""Helper script to detect system-level long path support on Windows.
2+
3+
Returns 0 if the long path support is enabled in the registry, or 1 otherwise.
4+
5+
Reference: https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=registry#registry-setting-to-enable-long-paths
6+
"""
7+
8+
# Copyright Contributors to the OpenImageIO project.
9+
# SPDX-License-Identifier: Apache-2.0
10+
# https://github.com/AcademySoftwareFoundation/OpenImageIO
11+
12+
import sys
13+
import winreg
14+
15+
_SUB_KEY = "SYSTEM\\CurrentControlSet\\Control\\FileSystem"
16+
_VALUE_NAME = "LongPathsEnabled"
17+
18+
19+
def main() -> int:
20+
try:
21+
with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, _SUB_KEY) as key:
22+
reg_value, value_type = winreg.QueryValueEx(key, _VALUE_NAME)
23+
except OSError:
24+
# Key does not exist
25+
return 1
26+
27+
# It's vanishingly unlikely that someone would stuff some other value type
28+
# in this key, but let's be paranoid.
29+
if value_type != winreg.REG_DWORD:
30+
return 1
31+
32+
return int(reg_value != 1)
33+
34+
35+
if __name__ == "__main__":
36+
sys.exit(main())
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
"""Test script for long path support in compiled Windows executables.
2+
3+
This script verifies that `oiiotool` can read paths longer than MAX_PATH (260)
4+
characters on Windows.
5+
6+
Reference: https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation
7+
"""
8+
9+
# Copyright Contributors to the OpenImageIO project.
10+
# SPDX-License-Identifier: Apache-2.0
11+
# https://github.com/AcademySoftwareFoundation/OpenImageIO
12+
13+
import argparse
14+
import os
15+
import shutil
16+
import subprocess
17+
import sys
18+
import tempfile
19+
from uuid import uuid4
20+
21+
_WINDOWS_MAX_PATH = 260
22+
23+
24+
def main(src_root: str, oiiotool_exe: str) -> int:
25+
"""
26+
Manufactures a long directory path within a temp. directory, copies a test
27+
image to it, and ensures `oiiotool` can read it without erroring.
28+
"""
29+
if not os.path.isfile(oiiotool_exe):
30+
print(f"ERROR: oiiotool path {oiiotool_exe!r} does not exist")
31+
return 1
32+
33+
test_image = "grid.tif"
34+
src_image_path = os.path.join(src_root, "testsuite", "common", test_image)
35+
36+
with tempfile.TemporaryDirectory() as test_dir:
37+
while len(test_dir) < _WINDOWS_MAX_PATH:
38+
test_dir = os.path.join(test_dir, str(uuid4()))
39+
os.makedirs(test_dir, exist_ok=True)
40+
41+
shutil.copy(src_image_path, test_dir)
42+
test_image_path = os.path.join(test_dir, test_image)
43+
44+
print("Testing long image path:", test_image_path, flush=True)
45+
result = subprocess.call([oiiotool_exe, test_image_path, "--printinfo"])
46+
47+
return result
48+
49+
50+
if __name__ == "__main__":
51+
parser = argparse.ArgumentParser(description="Windows long path test")
52+
parser.add_argument("--source-root", required=True)
53+
parser.add_argument("--oiiotool-path", required=True)
54+
args = parser.parse_args()
55+
56+
sys.exit(main(args.source_root, args.oiiotool_path))

0 commit comments

Comments
 (0)