Files
docview/cmake/StageWindowsWorker.cmake
T
2026-09-21 13:41:40 +09:00

256 lines
12 KiB
CMake

# Native script/include inputs: WORKER_EXECUTABLE, RUNTIME_LIBRARIES,
# SEARCH_DIRECTORIES and EXTRA_RUNTIME_LIBRARIES (explicit MSVC redistributables).
# No input is resolved through PATH. CMake's PE scanner searches the importing
# binary's directory, OS directories and the explicit DIRECTORIES list.
cmake_minimum_required(VERSION 3.24)
include("${CMAKE_CURRENT_LIST_DIR}/WriteWindowsRuntimeManifest.cmake")
function(docview_runtime_actual_path input output)
if(NOT IS_ABSOLUTE "${input}" OR NOT EXISTS "${input}" OR IS_SYMLINK "${input}")
message(FATAL_ERROR "Invalid worker runtime source: ${input}")
endif()
set(actual "${input}")
if(WIN32)
# CMake before 3.27 lowercases DLL result names. Recover on-disk casing
# before comparing an imported target against the scanner's result.
get_filename_component(parent "${input}" DIRECTORY)
get_filename_component(name "${input}" NAME)
string(TOLOWER "${name}" wanted)
file(GLOB siblings LIST_DIRECTORIES FALSE "${parent}/*")
set(matches)
foreach(sibling IN LISTS siblings)
get_filename_component(candidate "${sibling}" NAME)
string(TOLOWER "${candidate}" folded)
if(folded STREQUAL wanted)
list(APPEND matches "${sibling}")
endif()
endforeach()
list(LENGTH matches count)
if(NOT count EQUAL 1)
message(FATAL_ERROR "Ambiguous case-insensitive runtime source: ${input}")
endif()
list(GET matches 0 actual)
endif()
set(${output} "${actual}" PARENT_SCOPE)
endfunction()
# Pure package-copy step is independently testable with synthetic MZ fixtures.
# It does not claim that these files passed the native PE dependency scanner.
function(docview_stage_runtime_files executable sources)
docview_runtime_file_info("${executable}" EXE executable_name executable_size)
get_filename_component(directory "${executable}" DIRECTORY)
file(LOCK "${directory}/.docview-worker-runtime.lock" GUARD FUNCTION TIMEOUT 60 RESULT_VARIABLE locked)
if(NOT locked STREQUAL "0")
message(FATAL_ERROR "Cannot lock the worker runtime package directory: ${locked}")
endif()
set(names)
set(paths)
set(digests)
set(sizes)
set(total "${executable_size}")
foreach(input IN LISTS sources)
docview_runtime_actual_path("${input}" source)
docview_runtime_file_info("${source}" DLL name size)
string(TOLOWER "${name}" key)
list(FIND names "${key}" existing)
file(SHA256 "${source}" digest)
if(NOT existing EQUAL -1)
list(GET paths ${existing} previous)
get_filename_component(previous_name "${previous}" NAME)
list(GET digests ${existing} previous_digest)
if(NOT name STREQUAL previous_name OR NOT digest STREQUAL previous_digest)
message(FATAL_ERROR "Conflicting worker runtime DLL sources: ${previous};${source}")
endif()
continue()
endif()
list(APPEND names "${key}")
list(APPEND paths "${source}")
list(APPEND digests "${digest}")
list(APPEND sizes "${size}")
list(LENGTH names count)
math(EXPR total "${total} + ${size}")
if(count GREATER 127 OR total GREATER 536870912)
message(FATAL_ERROR "Worker runtime exceeds 128 files or 512 MiB")
endif()
endforeach()
# Inspect existing names case-insensitively, including on case-sensitive
# build hosts. Never overwrite a different DLL shared by another worker.
file(GLOB existing_entries LIST_DIRECTORIES TRUE "${directory}/*")
set(runtime_files "${executable}")
list(LENGTH paths count)
if(count GREATER 0)
math(EXPR last "${count} - 1")
foreach(index RANGE 0 ${last})
list(GET paths ${index} source)
list(GET names ${index} key)
list(GET digests ${index} digest)
list(GET sizes ${index} size)
get_filename_component(name "${source}" NAME)
set(destination "${directory}/${name}")
foreach(entry IN LISTS existing_entries)
get_filename_component(existing_name "${entry}" NAME)
string(TOLOWER "${existing_name}" existing_key)
if(existing_key STREQUAL key AND NOT existing_name STREQUAL name)
message(FATAL_ERROR "Case-insensitive runtime destination collision: ${name}")
endif()
endforeach()
if(EXISTS "${destination}" OR IS_SYMLINK "${destination}")
docview_runtime_file_info("${destination}" DLL old_name old_size)
file(SHA256 "${destination}" old_digest)
if(NOT old_size EQUAL size OR NOT old_digest STREQUAL digest)
message(FATAL_ERROR "Refusing to replace a different runtime DLL: ${destination}")
endif()
else()
string(RANDOM LENGTH 24 ALPHABET 0123456789abcdef nonce)
set(temporary "${destination}.${nonce}.tmp")
if(EXISTS "${temporary}" OR IS_SYMLINK "${temporary}")
message(FATAL_ERROR "Runtime copy temporary path already exists")
endif()
file(COPY_FILE "${source}" "${temporary}" RESULT copied)
if(NOT copied STREQUAL "0")
file(REMOVE "${temporary}")
message(FATAL_ERROR "Cannot copy runtime DLL: ${copied}")
endif()
file(SHA256 "${temporary}" copied_digest)
file(SIZE "${temporary}" copied_size)
if(NOT copied_digest STREQUAL digest OR NOT copied_size EQUAL size)
file(REMOVE "${temporary}")
message(FATAL_ERROR "Runtime DLL changed while copying: ${source}")
endif()
file(RENAME "${temporary}" "${destination}" NO_REPLACE RESULT renamed)
if(NOT renamed STREQUAL "0")
file(REMOVE "${temporary}")
message(FATAL_ERROR "Cannot publish runtime DLL: ${renamed}")
endif()
# Older CMake on POSIX can publish NO_REPLACE with a hard link
# while leaving the source name behind. Remove only our own
# temporary name after successful publication, preserving the
# destination and NO_REPLACE's collision protection.
file(REMOVE "${temporary}")
if(EXISTS "${temporary}" OR IS_SYMLINK "${temporary}")
message(FATAL_ERROR "Cannot remove the runtime copy temporary path")
endif()
endif()
list(APPEND runtime_files "${destination}")
endforeach()
endif()
docview_write_windows_runtime_manifest("${executable}" "${runtime_files}")
endfunction()
function(docview_regex_case_path path output)
string(REPLACE "\\" "/" normalized "${path}")
string(LENGTH "${normalized}" length)
math(EXPR last "${length} - 1")
set(regex)
set(metacharacters "." "[" "]" "(" ")" "+" "*" "?" "^" "$" "|" "{" "}")
foreach(index RANGE 0 ${last})
string(SUBSTRING "${normalized}" ${index} 1 character)
if(character MATCHES "[A-Za-z]")
string(TOUPPER "${character}" upper)
string(TOLOWER "${character}" lower)
string(APPEND regex "[${upper}${lower}]")
elseif(character IN_LIST metacharacters)
string(APPEND regex "\\${character}")
else()
string(APPEND regex "${character}")
endif()
endforeach()
set(${output} "${regex}" PARENT_SCOPE)
endfunction()
function(docview_is_windows_os_runtime path system_root output)
string(REPLACE "\\" "/" normalized "${path}")
string(REPLACE "\\" "/" root "${system_root}")
string(REGEX REPLACE "/+$" "" root "${root}")
string(TOLOWER "${normalized}" normalized)
string(TOLOWER "${root}/" root)
string(FIND "${normalized}" "${root}" prefix)
set(result FALSE)
if(prefix EQUAL 0)
string(LENGTH "${root}" length)
string(SUBSTRING "${normalized}" ${length} -1 relative)
if(relative MATCHES "^(system32|syswow64)/" OR relative MATCHES "^[^/]+[.]dll$")
set(result TRUE)
endif()
endif()
set(${output} "${result}" PARENT_SCOPE)
endfunction()
function(docview_stage_windows_worker)
if(NOT CMAKE_HOST_WIN32)
message(FATAL_ERROR "Native Windows PE runtime dependency scanning requires a Windows host")
endif()
docview_runtime_file_info("${WORKER_EXECUTABLE}" EXE executable_name executable_size)
file(TO_CMAKE_PATH "$ENV{SystemRoot}" system_root)
if(NOT IS_ABSOLUTE "${system_root}" OR NOT IS_DIRECTORY "${system_root}/System32")
message(FATAL_ERROR "Cannot locate the Windows OS runtime directories")
endif()
docview_regex_case_path("${system_root}" os_regex)
set(os_exclusions "^${os_regex}/[Ss][Yy][Ss][Tt][Ee][Mm]32/"
"^${os_regex}/[Ss][Yy][Ss][Ww][Oo][Ww]64/"
"^${os_regex}/[^/]+[.][Dd][Ll][Ll]$")
set(explicit_libraries)
set(directories ${SEARCH_DIRECTORIES})
foreach(input IN LISTS RUNTIME_LIBRARIES EXTRA_RUNTIME_LIBRARIES)
docview_runtime_actual_path("${input}" source)
docview_runtime_file_info("${source}" DLL name size)
string(TOLOWER "${name}" folded)
if(folded MATCHES "^(api|ext)-ms-win-.*[.]dll$")
message(FATAL_ERROR "API-set DLLs belong to Windows and must not be copied: ${name}")
endif()
docview_is_windows_os_runtime("${source}" "${system_root}" os_file)
if(os_file)
message(FATAL_ERROR "Do not package DLLs from Windows OS directories: ${source}")
endif()
list(APPEND explicit_libraries "${source}")
get_filename_component(parent "${source}" DIRECTORY)
list(APPEND directories "${parent}")
endforeach()
list(REMOVE_DUPLICATES explicit_libraries)
list(REMOVE_DUPLICATES directories)
foreach(directory IN LISTS directories)
if(NOT IS_ABSOLUTE "${directory}" OR NOT IS_DIRECTORY "${directory}")
message(FATAL_ERROR "DLL search directories must be explicit existing absolute paths: ${directory}")
endif()
endforeach()
set(CMAKE_GET_RUNTIME_DEPENDENCIES_PLATFORM "windows+pe")
if(DEFINED CMAKE_GET_RUNTIME_DEPENDENCIES_COMMAND AND
(NOT IS_ABSOLUTE "${CMAKE_GET_RUNTIME_DEPENDENCIES_COMMAND}" OR
NOT EXISTS "${CMAKE_GET_RUNTIME_DEPENDENCIES_COMMAND}"))
message(FATAL_ERROR "The PE inspection tool must have an existing absolute path")
endif()
file(GET_RUNTIME_DEPENDENCIES
EXECUTABLES "${WORKER_EXECUTABLE}"
LIBRARIES ${explicit_libraries}
DIRECTORIES ${directories}
RESOLVED_DEPENDENCIES_VAR resolved
UNRESOLVED_DEPENDENCIES_VAR unresolved
CONFLICTING_DEPENDENCIES_PREFIX conflicting
PRE_EXCLUDE_REGEXES "^api-ms-win-.*[.]dll$" "^ext-ms-win-.*[.]dll$"
POST_EXCLUDE_REGEXES ${os_exclusions})
if(unresolved)
message(FATAL_ERROR "Unresolved worker runtime DLL dependencies: ${unresolved}")
endif()
if(conflicting_FILENAMES)
message(FATAL_ERROR "Conflicting worker runtime DLL dependencies: ${conflicting_FILENAMES}")
endif()
set(package_dependencies)
foreach(dependency IN LISTS resolved)
docview_runtime_actual_path("${dependency}" dependency)
docview_is_windows_os_runtime("${dependency}" "${system_root}" os_file)
get_filename_component(name "${dependency}" NAME)
string(TOLOWER "${name}" folded)
if(NOT os_file AND NOT folded MATCHES "^(api|ext)-ms-win-.*[.]dll$")
list(APPEND package_dependencies "${dependency}")
endif()
endforeach()
# Explicit redistributables are included even if a matching import resolved
# to a Windows OS DLL that the scanner correctly excluded.
set(package_sources ${explicit_libraries} ${package_dependencies})
docview_stage_runtime_files("${WORKER_EXECUTABLE}" "${package_sources}")
endfunction()
if(DEFINED WORKER_EXECUTABLE)
docview_stage_windows_worker()
endif()