# 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()