From 4975f6e1f28721aaaff676365ccd3fec3b50d2bf Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Sat, 9 May 2026 11:18:33 -0700 Subject: [PATCH 1/3] build(cmake): invoke robocopy wrapper via cmd /c absolute path Modern Windows refuses to execute .cmd files from the current working directory unless they're prefixed with `.\` (introduced as a security default to prevent CWD command-hijacking). CMake's add_custom_command strips the absolute path from COMMAND args that live inside CMAKE_BINARY_DIR (the custom-build CWD), so the generated MSBuild command was just `call robocopy_wrapper.cmd ...` - which fails with "is not recognized as an internal or external command". Fix: invoke through `cmd /c ""`. Because the wrapper path is now an argument to cmd rather than the COMMAND itself, CMake preserves the absolute path verbatim and the generated MSBuild rule becomes `cmd /c E:\path\to\robocopy_wrapper.cmd ...` - which works. The variable rename (ROBOCOPY_WRAPPER -> ROBOCOPY_WRAPPER_PATH for the file path, ROBOCOPY_WRAPPER for the invocation list) keeps the three existing call sites untouched - they consume `${ROBOCOPY_WRAPPER}` as a command-list expansion and pick up the new `cmd /c ` prefix automatically. Verified end-to-end with BuildRelease.bat ALL-WITH-AUTO-DEPLOYMENT: both Skyrim VR and Skyrim Special Edition Data dirs receive the plugin DLL + shaders without errors. --- CMakeLists.txt | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index d5ef55de09..45dddb7667 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -352,13 +352,19 @@ string(TIMESTAMP UTC_NOW "%Y-%m-%dT%H-%MZ" UTC) # Set AIO directory path used by multiple targets below set(AIO_DIR "${CMAKE_CURRENT_BINARY_DIR}/aio") -# Robocopy wrapper for Windows incremental file copy (used by deployment targets) +# Robocopy wrapper for Windows incremental file copy (used by deployment targets). +# We invoke through `cmd /c ""` rather than the bare wrapper path because +# modern Windows refuses to execute scripts from the current directory without an explicit +# `.\` prefix, and CMake strips the absolute path on COMMAND args that live inside +# CMAKE_BINARY_DIR (the custom-build CWD). Treating the wrapper as an argument to cmd +# keeps the absolute path intact in the generated MSBuild command. if(WIN32) - set(ROBOCOPY_WRAPPER "${CMAKE_BINARY_DIR}/robocopy_wrapper.cmd") + set(ROBOCOPY_WRAPPER_PATH "${CMAKE_BINARY_DIR}/robocopy_wrapper.cmd") file( - WRITE ${ROBOCOPY_WRAPPER} + WRITE ${ROBOCOPY_WRAPPER_PATH} "@echo off\r\nrem Robocopy wrapper: forwards all args to robocopy and normalizes exit codes\r\nrobocopy %*\r\nset rc=%ERRORLEVEL%\r\nif %rc% GEQ 8 exit /b %rc%\r\nexit /b 0\r\n" ) + set(ROBOCOPY_WRAPPER cmd /c "${ROBOCOPY_WRAPPER_PATH}") endif() # ####################################################################################################################### From c8ca33ec4b27d0695f24d0a9b6ca2449ed45093f Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Sat, 9 May 2026 11:28:58 -0700 Subject: [PATCH 2/3] build(cmake): stop PREPARE_AIO from racing with copy_shaders on Shaders/ PREPARE_AIO and copy_shaders.stamp both staged shader files into ${AIO_DIR}/Shaders, with overlapping working sets: PREPARE_AIO : everything under package/* and each feature path */* copy_shaders : package/Shaders/* and each feature's Shaders/* When MSBuild builds them in parallel both rules call cmake -E copy_if_different on the same destination shader files. One process opens the file for write while the other tries the same and gets "Permission denied" on Windows (Windows file locking, not Unix-style overwrite). Symptom seen at PREPARE_AIO during a parallel build: Error copying file (if different) from "...features/Terrain Helper/TerrainHelper.esp" to ".../build/ALL/aio/TerrainHelper.esp": Permission denied (The visible failure happened on TerrainHelper.esp because that's where the loser landed; the underlying contention was on shader files copied earlier in the same parallel sweep.) Fix: exclude /Shaders/ from PREPARE_AIO's input lists so it owns only non-shader feature content, and copy_shaders.stamp owns Shaders/. The two rules now have disjoint working sets - no contention possible. Filter applied in three places: - input-tracking glob (DEPENDS list) - package source glob - per-feature source glob Bonus: PREPARE_AIO does less duplicate work, build is slightly faster. Verified end-to-end with BuildRelease.bat ALL-WITH-AUTO-DEPLOYMENT from a cleaned AIO dir: full configure + compile + parallel deploy without any Permission denied or race symptoms; plugin deployed to both Skyrim VR and Skyrim Special Edition Data dirs. --- CMakeLists.txt | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 45dddb7667..e91096e35c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -424,14 +424,20 @@ if(AUTO_PLUGIN_DEPLOYMENT OR AIO_ZIP_TO_DIST) # Prepare AIO only when sources change. Gather package + feature files as # inputs so the prepare step runs only when something actually changed. + # Shader files are intentionally excluded - copy_shaders.stamp owns those, + # and including them here causes a race where two parallel custom-build + # rules call cmake -E copy_if_different on the same destination file + # (Permission denied on Windows when one process has it open for write). file( GLOB_RECURSE _AIO_PACKAGE_FILES LIST_DIRECTORIES FALSE "${CMAKE_SOURCE_DIR}/package/*" ) list(FILTER _AIO_PACKAGE_FILES EXCLUDE REGEX "/Tests/") + list(FILTER _AIO_PACKAGE_FILES EXCLUDE REGEX "/Shaders/") foreach(_fpath IN LISTS FEATURE_PATHS) file(GLOB_RECURSE _tmp LIST_DIRECTORIES FALSE "${_fpath}/*") + list(FILTER _tmp EXCLUDE REGEX "/Shaders/") list(APPEND _AIO_PACKAGE_FILES ${_tmp}) endforeach() @@ -463,16 +469,21 @@ if(AUTO_PLUGIN_DEPLOYMENT OR AIO_ZIP_TO_DIST) "${CMAKE_SOURCE_DIR}/cmake/CleanupStaleEntries.cmake" ) - # Copy package files (exclude test files from production packages) + # Copy package files (exclude test files from production packages, and + # exclude shaders since copy_shaders.stamp owns those - see input-tracking + # comment above for the race-condition rationale). file( GLOB_RECURSE _AIO_PACKAGE_SOURCE_FILES LIST_DIRECTORIES FALSE "${CMAKE_SOURCE_DIR}/package/*" ) list(FILTER _AIO_PACKAGE_SOURCE_FILES EXCLUDE REGEX "/Tests/") + list(FILTER _AIO_PACKAGE_SOURCE_FILES EXCLUDE REGEX "/Shaders/") append_copy_if_different(_prepare_aio_cmds _AIO_PACKAGE_SOURCE_FILES "${CMAKE_SOURCE_DIR}/package" "${AIO_DIR}") - # Copy feature folders (only files, preserve existing files in AIO) + # Copy feature folders (only files, preserve existing files in AIO). + # Shader files are excluded - copy_shaders.stamp owns the Shaders/ subdir + # so the two custom-build rules don't race on the same destinations. foreach(_fpath IN LISTS FEATURE_PATHS) if(EXISTS "${_fpath}") file( @@ -480,6 +491,7 @@ if(AUTO_PLUGIN_DEPLOYMENT OR AIO_ZIP_TO_DIST) LIST_DIRECTORIES FALSE "${_fpath}/*" ) + list(FILTER _feature_files EXCLUDE REGEX "/Shaders/") append_copy_if_different(_prepare_aio_cmds _feature_files "${_fpath}" "${AIO_DIR}") endif() endforeach() From 83f47ffd50ad884f3626a3481e11545c88304470 Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Sat, 9 May 2026 13:35:54 -0700 Subject: [PATCH 3/3] build(cmake): depend AIO archive on copy_shaders.stamp PR #2306 excluded shaders from prepare_aio.stamp inputs to avoid a race with copy_shaders.stamp on the same destination files. Side effect: AIO_ZIP_PACKAGE only depended on PREPARE_AIO, so shader-only changes no longer triggered an archive rebuild and AIO_ZIP_TO_DIST builds could ship stale Shaders/ contents. Add copy_shaders.stamp to the AIO_ZIP_PACKAGE custom command's DEPENDS so the archive rebuilds whenever either the package files or the shaders change. Reported by CodeRabbit on PR #2306. --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index e91096e35c..5225ff76ea 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -924,7 +924,7 @@ if(AIO_ZIP_TO_DIST) COMMAND ${CMAKE_COMMAND} -E tar cf ${AIO_ARCHIVE} --format=7zip -- . COMMAND ${CMAKE_COMMAND} -E touch ${AIO_ZIP_STAMP} WORKING_DIRECTORY ${AIO_DIR} - DEPENDS PREPARE_AIO + DEPENDS PREPARE_AIO ${CMAKE_CURRENT_BINARY_DIR}/copy_shaders.stamp COMMENT "Creating AIO archive ${AIO_ARCHIVE}" )