From 5843ec510cc7902759de773789f9d35b4be97bed Mon Sep 17 00:00:00 2001 From: Patedam Date: Thu, 23 Jul 2026 14:09:52 -0400 Subject: [PATCH] make build go fast --- misc/build.bat | 207 ++++---- misc/build_helper.ps1 | 163 ------ misc/check_target.ps1 | 29 -- misc/clean_unity.ps1 | 61 --- misc/summary_helper.ps1 | 39 -- tools/build_check.cpp | 1048 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 1172 insertions(+), 375 deletions(-) delete mode 100644 misc/build_helper.ps1 delete mode 100644 misc/check_target.ps1 delete mode 100644 misc/clean_unity.ps1 delete mode 100644 misc/summary_helper.ps1 create mode 100644 tools/build_check.cpp diff --git a/misc/build.bat b/misc/build.bat index 793e956..0883b15 100644 --- a/misc/build.bat +++ b/misc/build.bat @@ -11,7 +11,8 @@ set SCRIPT_DIR=%~dp0 cd /d "%SCRIPT_DIR%.." set FBUILD=misc\fbuild.exe -set HELPER=powershell -NoProfile -ExecutionPolicy Bypass -File "%SCRIPT_DIR%build_helper.ps1" +set CHECK_TOOL=misc\build_check.exe +set CHECK_SRC=tools\build_check.cpp set TARGET_JULIET=0 set TARGET_ROMEO=0 @@ -37,20 +38,10 @@ shift goto parse_args :check_defaults -if !CONFIG_SPECIFIED! equ 0 ( - if !TARGET_SPECIFIED! equ 0 ( - set CONFIG_DEBUG=1& set CONFIG_RELEASE=1& set CONFIG_PROFILE=1 - set TARGET_JULIET=1& set TARGET_GAME=1& set TARGET_ROMEO=1& set TARGET_SHADER=1 - goto start_build - ) - set CONFIG_DEBUG=1 -) -if !TARGET_SPECIFIED! equ 0 ( - set TARGET_JULIET=1& set TARGET_GAME=1& set TARGET_ROMEO=1& set TARGET_SHADER=1 -) +if !CONFIG_SPECIFIED! equ 0 set CONFIG_DEBUG=1& if !TARGET_SPECIFIED! equ 0 set CONFIG_RELEASE=1& set CONFIG_PROFILE=1 +if !TARGET_SPECIFIED! equ 0 set TARGET_JULIET=1& set TARGET_GAME=1& set TARGET_ROMEO=1& set TARGET_SHADER=1 :start_build -%HELPER% -Action StartBuild >nul 2>&1 if exist "Intermediate\built_outputs.tmp" del /f /q "Intermediate\built_outputs.tmp" >nul 2>&1 if not exist "%FBUILD%" ( @@ -58,6 +49,7 @@ if not exist "%FBUILD%" ( exit /b 1 ) +:: Find Clang / MSVC compiler set COMPILER=clang-cl.exe where %COMPILER% >nul 2>nul if !ERRORLEVEL! neq 0 ( @@ -69,21 +61,72 @@ if !ERRORLEVEL! neq 0 ( ) ) -if !CONFIG_DEBUG! equ 1 call :build_config Debug -if !ERRORLEVEL! neq 0 exit /b !ERRORLEVEL! +:: Check if build_check.exe needs to be compiled (missing, version changed, or source changed) +set CHECK_TOOL_VER=3 +set NEED_RECOMPILE_CHECK_TOOL=0 +if not exist "%CHECK_TOOL%" ( + set NEED_RECOMPILE_CHECK_TOOL=1 +) else ( + set "CURR_VER=" + if exist "Intermediate\build_check.ver" ( + for /f "usebackq delims=" %%I in ("Intermediate\build_check.ver") do set "CURR_VER=%%I" + ) + if not "!CURR_VER!"=="!CHECK_TOOL_VER!" set NEED_RECOMPILE_CHECK_TOOL=1 + set "SRC_TIME=" + for %%I in ("%CHECK_SRC%") do set "SRC_TIME=%%~tI" + set "STAMP_TIME=" + if exist "Intermediate\build_check.stamp" ( + for /f "usebackq delims=" %%I in ("Intermediate\build_check.stamp") do set "STAMP_TIME=%%I" + ) + if not "!SRC_TIME!"=="!STAMP_TIME!" set NEED_RECOMPILE_CHECK_TOOL=1 +) -if !CONFIG_PROFILE! equ 1 call :build_config Profile -if !ERRORLEVEL! neq 0 exit /b !ERRORLEVEL! +if !NEED_RECOMPILE_CHECK_TOOL! equ 1 ( + if exist "Intermediate\build_check.stamp" del /f /q "Intermediate\build_check.stamp" + if exist "Intermediate\build_check.ver" del /f /q "Intermediate\build_check.ver" + if exist "%CHECK_TOOL%" ( + del /f /q "%CHECK_TOOL%" >nul 2>&1 + if exist "%CHECK_TOOL%" ( + echo [BUILD FAILED] Could not overwrite %CHECK_TOOL%. It may be in use by another process. + exit /b 1 + ) + ) + echo [BUILD] Compiling fast native helper tool [tools\build_check.cpp -^> misc\build_check.exe]... + %COMPILER% /nologo /std:c++20 /MT /O2 /EHa- /W4 "%CHECK_SRC%" /Fe"%CHECK_TOOL%" + if !ERRORLEVEL! neq 0 ( + echo [BUILD FAILED] Could not compile build helper tool. + exit /b 1 + ) + if not exist "%CHECK_TOOL%" ( + echo [BUILD FAILED] Compilation failed to produce %CHECK_TOOL%. + exit /b 1 + ) + "%CHECK_TOOL%" check_alive >nul 2>&1 + if !ERRORLEVEL! neq 0 ( + echo [BUILD FAILED] %CHECK_TOOL% was built but failed to execute ^(missing DLL or AV block^). + exit /b 1 + ) + if not exist "Intermediate" mkdir "Intermediate" + (echo !CHECK_TOOL_VER!)>"Intermediate\build_check.ver" + for %%I in ("%CHECK_SRC%") do (echo %%~tI)>"Intermediate\build_check.stamp" +) -if !CONFIG_RELEASE! equ 1 call :build_config Release -if !ERRORLEVEL! neq 0 exit /b !ERRORLEVEL! +"%CHECK_TOOL%" start_build +if !ERRORLEVEL! neq 0 ( + echo [BUILD FAILED] %CHECK_TOOL% start_build failed with exit code !ERRORLEVEL!. + exit /b 1 +) + +if !CONFIG_DEBUG! equ 1 call :build_config Debug || exit /b !ERRORLEVEL! +if !CONFIG_PROFILE! equ 1 call :build_config Profile || exit /b !ERRORLEVEL! +if !CONFIG_RELEASE! equ 1 call :build_config Release || exit /b !ERRORLEVEL! echo. echo ============================================================================== echo [BUILD SUMMARY] echo ============================================================================== echo Compiler Used : %COMPILER% -%HELPER% -Action FinishBuild +"%CHECK_TOOL%" finish_build echo ============================================================================== echo [ALL BUILDS SUCCEEDED] All requested targets processed successfully. echo ============================================================================== @@ -107,16 +150,23 @@ if !TARGET_GAME! equ 1 set FASTBUILD_TARGETS=!FASTBUILD_TARGETS! Game-Unity if !TARGET_ROMEO! equ 1 set FASTBUILD_TARGETS=!FASTBUILD_TARGETS! Romeo-Unity if !TARGET_SHADER! equ 1 set FASTBUILD_TARGETS=!FASTBUILD_TARGETS! JulietShaderCompiler-Unity -%HELPER% -Action StartUnity >nul 2>&1 - +"%CHECK_TOOL%" start_step "FASTBuild Unity Gen [!CFG!]" "%FBUILD%" !FASTBUILD_TARGETS! -if !ERRORLEVEL! neq 0 ( +set "FBUILD_ERR=!ERRORLEVEL!" +"%CHECK_TOOL%" end_step +if !FBUILD_ERR! neq 0 ( echo [BUILD FAILED] FASTBuild failed to generate unity files for %CFG%. - exit /b !ERRORLEVEL! + exit /b !FBUILD_ERR! ) -:: Evaluate all target needs in a single fast PowerShell pass -%HELPER% -Action EvaluateConfig -CFG "!CFG!" -TargetJuliet "!TARGET_JULIET!" -TargetGame "!TARGET_GAME!" -TargetRomeo "!TARGET_ROMEO!" -TargetShader "!TARGET_SHADER!" >nul 2>&1 +:: Evaluate all target needs in a single fast native pass (<5ms) +"%CHECK_TOOL%" start_step "Config Evaluation [!CFG!]" +"%CHECK_TOOL%" evaluate "!CFG!" "!TARGET_JULIET!" "!TARGET_GAME!" "!TARGET_ROMEO!" "!TARGET_SHADER!" +if !ERRORLEVEL! neq 0 ( + echo [BUILD FAILED] %CHECK_TOOL% evaluate failed with exit code !ERRORLEVEL!. + exit /b 1 +) +"%CHECK_TOOL%" end_step set FASTBUILD_UNITY_CHANGED=0 set JULIET_NEED_COMPILE=0 @@ -128,7 +178,7 @@ set SHADER_NEED_COMPILE=0 if exist "Intermediate\config_status.tmp" ( for /f "usebackq tokens=1,2 delims==" %%A in ("Intermediate\config_status.tmp") do set %%A=%%B - del /f /q "Intermediate\config_status.tmp" >nul 2>&1 + del /f /q "Intermediate\config_status.tmp" ) if !FASTBUILD_UNITY_CHANGED! equ 1 ( @@ -138,12 +188,9 @@ if !FASTBUILD_UNITY_CHANGED! equ 1 ( ) set ANY_NEED_BUILD=0 -if !JULIET_NEED_COMPILE! equ 1 set ANY_NEED_BUILD=1 -if !IMGUI_NEED_COMPILE! equ 1 set ANY_NEED_BUILD=1 -if !GAME_NEED_COMPILE! equ 1 set ANY_NEED_BUILD=1 -if !JULIETAPP_NEED_COMPILE! equ 1 set ANY_NEED_BUILD=1 -if !ROMEO_NEED_COMPILE! equ 1 set ANY_NEED_BUILD=1 -if !SHADER_NEED_COMPILE! equ 1 set ANY_NEED_BUILD=1 +for %%T in (JULIET IMGUI GAME JULIETAPP ROMEO SHADER) do ( + if !%%T_NEED_COMPILE! equ 1 set ANY_NEED_BUILD=1 +) if !ANY_NEED_BUILD! equ 0 ( echo [SKIP / UP-TO-DATE] All requested targets in [%CFG%] are up-to-date. @@ -180,8 +227,11 @@ if !JULIET_NEED_COMPILE! equ 0 ( echo [SKIP / UP-TO-DATE] Intermediate\x64-!CFG!\Juliet_Unity*.obj [Unity files unchanged] ) else ( echo [REBUILDING] Compiling Intermediate\x64-!CFG!\Juliet_Unity*.obj... + "%CHECK_TOOL%" start_step "Compiling Juliet Shared Objs [!CFG!]" %COMPILER% %COMPILER_FLAGS% /DJULIET_EXPORT /c Intermediate\Juliet\*.cpp /Fo"Intermediate\x64-!CFG!\\" - if !ERRORLEVEL! neq 0 exit /b !ERRORLEVEL! + set "COMP_ERR=!ERRORLEVEL!" + "%CHECK_TOOL%" end_step + if !COMP_ERR! neq 0 exit /b !COMP_ERR! ) :: --- ImGui Shared Objs --- @@ -190,8 +240,11 @@ if defined IMGUI_UNITY_SRCS ( echo [SKIP / UP-TO-DATE] Intermediate\x64-!CFG!\ImGui_Unity*.obj [Unity files unchanged] ) else ( echo [REBUILDING] Compiling Intermediate\x64-!CFG!\ImGui_Unity*.obj... + "%CHECK_TOOL%" start_step "Compiling ImGui Shared Objs [!CFG!]" %COMPILER% %COMPILER_FLAGS% /c !IMGUI_UNITY_SRCS! /Fo"Intermediate\x64-!CFG!\\" - if !ERRORLEVEL! neq 0 exit /b !ERRORLEVEL! + set "COMP_ERR=!ERRORLEVEL!" + "%CHECK_TOOL%" end_step + if !COMP_ERR! neq 0 exit /b !COMP_ERR! ) ) @@ -205,68 +258,56 @@ if !TARGET_GAME! equ 1 set BUILD_GAME_DLL=1 :: --- Game.dll --- if !BUILD_GAME_DLL! equ 1 ( - echo. - echo --- Building Game.dll [%CFG%] --- - if !GAME_NEED_COMPILE! equ 0 ( - echo [SKIP / UP-TO-DATE] bin\x64-!CFG!\Game.dll [Binary up to date, skipping compilation] - ) else ( - echo [REBUILDING] Compiling bin\x64-!CFG!\Game.dll... - %COMPILER% %COMPILER_FLAGS% /DJULIET_EXPORT /LD /Fe"bin\x64-!CFG!\Game.dll" ^ - %JULIET_OBJS% Intermediate\Game\*.cpp %IMGUI_OBJS% ^ - /link %COMMON_LIBS% /INCREMENTAL /ILK:"Intermediate\x64-!CFG!\Game.ilk" /PDB:"Intermediate\x64-!CFG!\Game.pdb" - if !ERRORLEVEL! neq 0 exit /b !ERRORLEVEL! - echo bin\x64-!CFG!\Game.dll>>Intermediate\built_outputs.tmp - ) + call :link_target "Game.dll" "Building Game.dll [!CFG!]" "!GAME_NEED_COMPILE!" "%COMPILER_FLAGS% /DJULIET_EXPORT /LD" "%JULIET_OBJS% Intermediate\Game\*.cpp %IMGUI_OBJS%" "/link %COMMON_LIBS% /INCREMENTAL /ILK:\"Intermediate\x64-!CFG!\Game.ilk\" /PDB:\"Intermediate\x64-!CFG!\Game.pdb\"" + if !ERRORLEVEL! neq 0 exit /b !ERRORLEVEL! ) :: --- JulietApp.exe --- if !TARGET_JULIET! equ 1 ( - echo. - echo --- Building JulietApp.exe [%CFG%] --- - if !JULIETAPP_NEED_COMPILE! equ 0 ( - echo [SKIP / UP-TO-DATE] bin\x64-!CFG!\JulietApp.exe [Binary up to date, skipping compilation] - ) else ( - echo [REBUILDING] Compiling bin\x64-!CFG!\JulietApp.exe... - %COMPILER% %COMPILER_FLAGS% /Fe"bin\x64-!CFG!\JulietApp.exe" ^ - Intermediate\JulietApp\*.cpp %IMGUI_OBJS% ^ - /link %COMMON_LIBS% bin\x64-!CFG!\Game.lib /SUBSYSTEM:CONSOLE /INCREMENTAL /ILK:"Intermediate\x64-!CFG!\JulietApp.ilk" /PDB:"Intermediate\x64-!CFG!\JulietApp.pdb" - if !ERRORLEVEL! neq 0 exit /b !ERRORLEVEL! - echo bin\x64-!CFG!\JulietApp.exe>>Intermediate\built_outputs.tmp - ) + call :link_target "JulietApp.exe" "Building JulietApp.exe [!CFG!]" "!JULIETAPP_NEED_COMPILE!" "%COMPILER_FLAGS%" "Intermediate\JulietApp\*.cpp %IMGUI_OBJS%" "/link %COMMON_LIBS% bin\x64-!CFG!\Game.lib /SUBSYSTEM:CONSOLE /INCREMENTAL /ILK:\"Intermediate\x64-!CFG!\JulietApp.ilk\" /PDB:\"Intermediate\x64-!CFG!\JulietApp.pdb\"" + if !ERRORLEVEL! neq 0 exit /b !ERRORLEVEL! ) :: --- Romeo.exe --- if !TARGET_ROMEO! equ 1 ( - echo. - echo --- Building Romeo.exe [%CFG%] --- - if !ROMEO_NEED_COMPILE! equ 0 ( - echo [SKIP / UP-TO-DATE] bin\x64-!CFG!\Romeo.exe [Binary up to date, skipping compilation] - ) else ( - echo [REBUILDING] Compiling bin\x64-!CFG!\Romeo.exe... - %COMPILER% %COMPILER_FLAGS% /Fe"bin\x64-!CFG!\Romeo.exe" ^ - %JULIET_OBJS% Intermediate\Romeo\*.cpp %IMGUI_OBJS% ^ - /link %COMMON_LIBS% /SUBSYSTEM:CONSOLE /INCREMENTAL /ILK:"Intermediate\x64-!CFG!\Romeo.ilk" /PDB:"Intermediate\x64-!CFG!\Romeo.pdb" - if !ERRORLEVEL! neq 0 exit /b !ERRORLEVEL! - echo bin\x64-!CFG!\Romeo.exe>>Intermediate\built_outputs.tmp - ) + call :link_target "Romeo.exe" "Building Romeo.exe [!CFG!]" "!ROMEO_NEED_COMPILE!" "%COMPILER_FLAGS%" "%JULIET_OBJS% Intermediate\Romeo\*.cpp %IMGUI_OBJS%" "/link %COMMON_LIBS% /SUBSYSTEM:CONSOLE /INCREMENTAL /ILK:\"Intermediate\x64-!CFG!\Romeo.ilk\" /PDB:\"Intermediate\x64-!CFG!\Romeo.pdb\"" + if !ERRORLEVEL! neq 0 exit /b !ERRORLEVEL! ) :: --- JulietShaderCompiler.exe --- if !TARGET_SHADER! equ 1 ( - echo. - echo --- Building JulietShaderCompiler.exe [%CFG%] --- if exist "Intermediate\JulietShaderCompiler\*.cpp" ( - if !SHADER_NEED_COMPILE! equ 0 ( - echo [SKIP / UP-TO-DATE] bin\x64-!CFG!\JulietShaderCompiler.exe [Binary up to date, skipping compilation] - ) else ( - echo [REBUILDING] Compiling bin\x64-!CFG!\JulietShaderCompiler.exe... - %COMPILER% %COMPILER_FLAGS% /I"JulietShaderCompiler" /Fe"bin\x64-!CFG!\JulietShaderCompiler.exe" ^ - %JULIET_OBJS% Intermediate\JulietShaderCompiler\*.cpp %IMGUI_OBJS% ^ - /link %COMMON_LIBS% dxcompiler.lib /DELAYLOAD:dxcompiler.dll /SUBSYSTEM:CONSOLE /INCREMENTAL /ILK:"Intermediate\x64-!CFG!\JulietShaderCompiler.ilk" /PDB:"Intermediate\x64-!CFG!\JulietShaderCompiler.pdb" - if !ERRORLEVEL! neq 0 exit /b !ERRORLEVEL! - echo bin\x64-!CFG!\JulietShaderCompiler.exe>>Intermediate\built_outputs.tmp - ) + call :link_target "JulietShaderCompiler.exe" "Building JulietShaderCompiler.exe [!CFG!]" "!SHADER_NEED_COMPILE!" "%COMPILER_FLAGS% /I\"JulietShaderCompiler\"" "%JULIET_OBJS% Intermediate\JulietShaderCompiler\*.cpp %IMGUI_OBJS%" "/link %COMMON_LIBS% dxcompiler.lib /DELAYLOAD:dxcompiler.dll /SUBSYSTEM:CONSOLE /INCREMENTAL /ILK:\"Intermediate\x64-!CFG!\JulietShaderCompiler.ilk\" /PDB:\"Intermediate\x64-!CFG!\JulietShaderCompiler.pdb\"" + if !ERRORLEVEL! neq 0 exit /b !ERRORLEVEL! ) ) exit /b 0 + +:: ------------------------------------------------------------------------------ +:: Helper Subroutine: Link / Build Target Binary +:: ------------------------------------------------------------------------------ +:link_target +set "TARGET_NAME=%~1" +set "STEP_TITLE=%~2" +set "NEED_COMPILE=%~3" +set "EXTRA_FLAGS=%~4" +set "SOURCES_AND_OBJS=%~5" +set "LINK_FLAGS=%~6" + +echo. +echo --- Building %TARGET_NAME% [%CFG%] --- +if "%NEED_COMPILE%"=="0" ( + echo [SKIP / UP-TO-DATE] bin\x64-!CFG!\%TARGET_NAME% [Binary up to date, skipping compilation] + exit /b 0 +) + +echo [REBUILDING] Compiling bin\x64-!CFG!\%TARGET_NAME%... +"%CHECK_TOOL%" start_step "%STEP_TITLE%" +%COMPILER% %EXTRA_FLAGS% /Fe"bin\x64-!CFG!\%TARGET_NAME%" %SOURCES_AND_OBJS% %LINK_FLAGS% +set "COMP_ERR=!ERRORLEVEL!" +"%CHECK_TOOL%" end_step +if !COMP_ERR! neq 0 exit /b !COMP_ERR! + +echo bin\x64-!CFG!\%TARGET_NAME%>>Intermediate\built_outputs.tmp +exit /b 0 diff --git a/misc/build_helper.ps1 b/misc/build_helper.ps1 deleted file mode 100644 index 3fba66a..0000000 --- a/misc/build_helper.ps1 +++ /dev/null @@ -1,163 +0,0 @@ -# misc/build_helper.ps1 - Unified Build Utility Script -param( - [string]$Action, - [string]$CFG, - [string]$TargetJuliet = "0", - [string]$TargetGame = "0", - [string]$TargetRomeo = "0", - [string]$TargetShader = "0" -) - -if (-not (Test-Path "Intermediate")) { - New-Item -ItemType Directory -Path "Intermediate" | Out-Null -} - -switch ($Action) { - "StartBuild" { - [System.IO.File]::WriteAllText("Intermediate\build_start.tmp", [System.DateTime]::Now.Ticks.ToString()) - } - - "FinishBuild" { - $elapsedStr = "00:00:00.000" - if (Test-Path "Intermediate\build_start.tmp") { - $startStr = Get-Content "Intermediate\build_start.tmp" -ErrorAction SilentlyContinue - if ($startStr) { - $startTicks = [long]$startStr - $elapsed = New-Object System.TimeSpan ([System.DateTime]::Now.Ticks - $startTicks) - $elapsedStr = "{0:D2}:{1:D2}:{2:D2}.{3:D3}" -f $elapsed.Hours, $elapsed.Minutes, $elapsed.Seconds, $elapsed.Milliseconds - } - Remove-Item "Intermediate\build_start.tmp" -ErrorAction SilentlyContinue - } - Write-Host "Total Duration : $elapsedStr" - Write-Host "Output Binaries:" - if (Test-Path "Intermediate\built_outputs.tmp") { - $files = Get-Content "Intermediate\built_outputs.tmp" -ErrorAction SilentlyContinue | Where-Object { Test-Path $_ } - if ($files) { - foreach ($f in $files) { - $item = Get-Item $f - $size = "{0:N2} MB" -f ($item.Length / 1MB) - $relPath = $item.FullName.Replace((Get-Location).Path + "\", "") - Write-Host " - $relPath ($size)" - } - } else { - Write-Host " - None built in this session (all requested targets up-to-date)" - } - Remove-Item "Intermediate\built_outputs.tmp" -ErrorAction SilentlyContinue - } else { - Write-Host " - None built in this session (all requested targets up-to-date)" - } - } - - "StartUnity" { - # Compute SHA256 hashes of all existing Unity files before FASTBuild runs - $hashes = @{} - Get-ChildItem -Path "Intermediate" -Recurse -Filter "*_Unity*.cpp" -ErrorAction SilentlyContinue | ForEach-Object { - $hash = (Get-FileHash -Path $_.FullName -Algorithm SHA256).Hash - $hashes[$_.FullName] = $hash - } - $hashes | ConvertTo-Json | Set-Content -Path "Intermediate\unity_hashes.tmp" -ErrorAction SilentlyContinue - } - - "EvaluateConfig" { - # 1. Strip '#pragma message' from generated Unity files if present - Get-ChildItem -Path "Intermediate" -Recurse -Filter "*_Unity*.cpp" -ErrorAction SilentlyContinue | ForEach-Object { - $filePath = $_.FullName - $lines = [System.IO.File]::ReadAllLines($filePath) - $filtered = $lines | Where-Object { $_ -notlike '*#pragma message*' } - if ($lines.Count -ne $filtered.Count) { - [System.IO.File]::WriteAllLines($filePath, $filtered) - } - } - - # 2. Compare Unity hashes to detect Unity structure changes - $oldHashes = @{} - if (Test-Path "Intermediate\unity_hashes.tmp") { - try { - $json = Get-Content "Intermediate\unity_hashes.tmp" -Raw -ErrorAction SilentlyContinue - if ($json) { - $obj = $json | ConvertFrom-Json - foreach ($prop in $obj.PSObject.Properties) { - $oldHashes[$prop.Name] = $prop.Value - } - } - } catch {} - Remove-Item "Intermediate\unity_hashes.tmp" -ErrorAction SilentlyContinue - } - - $unityChanged = "0" - $currentFiles = Get-ChildItem -Path "Intermediate" -Recurse -Filter "*_Unity*.cpp" -ErrorAction SilentlyContinue - if (-not $currentFiles) { - $unityChanged = "1" - } else { - foreach ($file in $currentFiles) { - $path = $file.FullName - $newHash = (Get-FileHash -Path $path -Algorithm SHA256).Hash - if (-not $oldHashes.ContainsKey($path) -or $oldHashes[$path] -ne $newHash) { - $unityChanged = "1" - break - } - } - } - - # Helper function to check if target binary/obj is missing or older than source files in dirs - function Test-NeedRebuild([string]$targetFile, [string[]]$dirs) { - if (-not (Test-Path $targetFile)) { return "1" } - if ($unityChanged -eq "1") { return "1" } - $targetTime = (Get-Item $targetFile).LastWriteTime - foreach ($d in $dirs) { - if (Test-Path $d) { - $newer = Get-ChildItem -Path $d -Recurse -File -ErrorAction SilentlyContinue | Where-Object { - ($_.Extension -eq '.cpp' -or $_.Extension -eq '.h' -or $_.Extension -eq '.hpp' -or $_.Extension -eq '.inl') -and $_.LastWriteTime -gt $targetTime - } - if ($newer) { return "1" } - } - } - return "0" - } - - $julietObj = "Intermediate\x64-$CFG\Juliet_Unity1.obj" - $imguiObj = "Intermediate\x64-$CFG\ImGui_Unity1.obj" - $gameDll = "bin\x64-$CFG\Game.dll" - $julietApp = "bin\x64-$CFG\JulietApp.exe" - $romeoExe = "bin\x64-$CFG\Romeo.exe" - $shaderExe = "bin\x64-$CFG\JulietShaderCompiler.exe" - - $julietNeed = Test-NeedRebuild $julietObj @("Juliet") - $imguiNeed = Test-NeedRebuild $imguiObj @("External/imgui") - - $gameNeed = "0" - if ($TargetJuliet -eq "1" -or $TargetGame -eq "1") { - $gameNeed = Test-NeedRebuild $gameDll @("Juliet", "Game", "External/imgui") - if ($julietNeed -eq "1") { $gameNeed = "1" } - } - - $julietAppNeed = "0" - if ($TargetJuliet -eq "1") { - $julietAppNeed = Test-NeedRebuild $julietApp @("JulietApp", "Game", "Juliet", "External/imgui") - if ($gameNeed -eq "1") { $julietAppNeed = "1" } - } - - $romeoNeed = "0" - if ($TargetRomeo -eq "1") { - $romeoNeed = Test-NeedRebuild $romeoExe @("Romeo", "Juliet", "External/imgui") - if ($julietNeed -eq "1") { $romeoNeed = "1" } - } - - $shaderNeed = "0" - if ($TargetShader -eq "1" -and (Test-Path "Intermediate\JulietShaderCompiler\*.cpp")) { - $shaderNeed = Test-NeedRebuild $shaderExe @("JulietShaderCompiler", "Juliet", "External/imgui") - if ($julietNeed -eq "1") { $shaderNeed = "1" } - } - - $lines = @( - "FASTBUILD_UNITY_CHANGED=$unityChanged", - "JULIET_NEED_COMPILE=$julietNeed", - "IMGUI_NEED_COMPILE=$imguiNeed", - "GAME_NEED_COMPILE=$gameNeed", - "JULIETAPP_NEED_COMPILE=$julietAppNeed", - "ROMEO_NEED_COMPILE=$romeoNeed", - "SHADER_NEED_COMPILE=$shaderNeed" - ) - $lines | Set-Content -Path "Intermediate\config_status.tmp" - } -} diff --git a/misc/check_target.ps1 b/misc/check_target.ps1 deleted file mode 100644 index 51c394f..0000000 --- a/misc/check_target.ps1 +++ /dev/null @@ -1,29 +0,0 @@ -# check_target.ps1 - Checks if target binary needs to be rebuilt based on binary existence and source file modification times -param( - [string]$TargetBinary, - [string]$SearchDirs -) - -if (-not (Test-Path $TargetBinary)) { - Set-Content -Path "Intermediate\need_build.tmp" -Value "1" - exit 0 -} - -$binTime = (Get-Item $TargetBinary).LastWriteTime -$dirs = $SearchDirs -split ',' - -$needsRebuild = "0" -foreach ($dir in $dirs) { - if (Test-Path $dir) { - # Check source .cpp, .h, .hpp, .inl files for changes newer than target binary - $newer = Get-ChildItem -Path $dir -Recurse -File -ErrorAction SilentlyContinue | Where-Object { - ($_.Extension -eq '.cpp' -or $_.Extension -eq '.h' -or $_.Extension -eq '.hpp' -or $_.Extension -eq '.inl') -and $_.LastWriteTime -gt $binTime - } - if ($newer) { - $needsRebuild = "1" - break - } - } -} - -Set-Content -Path "Intermediate\need_build.tmp" -Value $needsRebuild diff --git a/misc/clean_unity.ps1 b/misc/clean_unity.ps1 deleted file mode 100644 index f9490be..0000000 --- a/misc/clean_unity.ps1 +++ /dev/null @@ -1,61 +0,0 @@ -# clean_unity.ps1 - Manages Unity files content hashing and change detection -param( - [string]$Action = "Finish" -) - -if (-not (Test-Path "Intermediate")) { New-Item -ItemType Directory -Path "Intermediate" | Out-Null } - -$hashFile = "Intermediate\unity_hashes.tmp" - -if ($Action -eq "Start") { - # Compute SHA256 hashes of all existing Unity files before FASTBuild runs - $hashes = @{} - Get-ChildItem -Path "Intermediate" -Recurse -Filter "*_Unity*.cpp" -ErrorAction SilentlyContinue | ForEach-Object { - $hash = (Get-FileHash -Path $_.FullName -Algorithm SHA256).Hash - $hashes[$_.FullName] = $hash - } - $hashes | ConvertTo-Json | Set-Content -Path $hashFile -ErrorAction SilentlyContinue -} -elseif ($Action -eq "Finish") { - $oldHashes = @{} - if (Test-Path $hashFile) { - try { - $json = Get-Content $hashFile -Raw -ErrorAction SilentlyContinue - if ($json) { - $obj = $json | ConvertFrom-Json - foreach ($prop in $obj.PSObject.Properties) { - $oldHashes[$prop.Name] = $prop.Value - } - } - } catch {} - Remove-Item $hashFile -ErrorAction SilentlyContinue - } - - # Clean '#pragma message' from generated Unity files if present - Get-ChildItem -Path "Intermediate" -Recurse -Filter "*_Unity*.cpp" -ErrorAction SilentlyContinue | ForEach-Object { - $filePath = $_.FullName - $lines = [System.IO.File]::ReadAllLines($filePath) - $filtered = $lines | Where-Object { $_ -notlike '*#pragma message*' } - if ($lines.Count -ne $filtered.Count) { - [System.IO.File]::WriteAllLines($filePath, $filtered) - } - } - - # Detect if any Unity file is new or has changed content - $unityChanged = "0" - $currentFiles = Get-ChildItem -Path "Intermediate" -Recurse -Filter "*_Unity*.cpp" -ErrorAction SilentlyContinue - if (-not $currentFiles) { - $unityChanged = "1" - } else { - foreach ($file in $currentFiles) { - $path = $file.FullName - $newHash = (Get-FileHash -Path $path -Algorithm SHA256).Hash - if (-not $oldHashes.ContainsKey($path) -or $oldHashes[$path] -ne $newHash) { - $unityChanged = "1" - break - } - } - } - - Set-Content -Path "Intermediate\unity_changed.tmp" -Value $unityChanged -} diff --git a/misc/summary_helper.ps1 b/misc/summary_helper.ps1 deleted file mode 100644 index 459b7da..0000000 --- a/misc/summary_helper.ps1 +++ /dev/null @@ -1,39 +0,0 @@ -# summary_helper.ps1 - Handles build timing and summary reporting cleanly -param( - [string]$Action -) - -if ($Action -eq "Start") { - if (-not (Test-Path "Intermediate")) { New-Item -ItemType Directory -Path "Intermediate" | Out-Null } - [System.IO.File]::WriteAllText("Intermediate\build_start.tmp", [System.DateTime]::Now.Ticks.ToString()) -} -elseif ($Action -eq "Finish") { - $elapsedStr = "00:00:00.000" - if (Test-Path "Intermediate\build_start.tmp") { - $startStr = Get-Content "Intermediate\build_start.tmp" -ErrorAction SilentlyContinue - if ($startStr) { - $startTicks = [long]$startStr - $elapsed = New-Object System.TimeSpan ([System.DateTime]::Now.Ticks - $startTicks) - $elapsedStr = "{0:D2}:{1:D2}:{2:D2}.{3:D3}" -f $elapsed.Hours, $elapsed.Minutes, $elapsed.Seconds, $elapsed.Milliseconds - } - Remove-Item "Intermediate\build_start.tmp" -ErrorAction SilentlyContinue - } - Write-Host "Total Duration : $elapsedStr" - Write-Host "Output Binaries:" - if (Test-Path "Intermediate\built_outputs.tmp") { - $files = Get-Content "Intermediate\built_outputs.tmp" -ErrorAction SilentlyContinue | Where-Object { Test-Path $_ } - if ($files) { - foreach ($f in $files) { - $item = Get-Item $f - $size = "{0:N2} MB" -f ($item.Length / 1MB) - $relPath = $item.FullName.Replace((Get-Location).Path + "\", "") - Write-Host " - $relPath ($size)" - } - } else { - Write-Host " - None built in this session (all requested targets up-to-date)" - } - Remove-Item "Intermediate\built_outputs.tmp" -ErrorAction SilentlyContinue - } else { - Write-Host " - None built in this session (all requested targets up-to-date)" - } -} diff --git a/tools/build_check.cpp b/tools/build_check.cpp new file mode 100644 index 0000000..9a5ad08 --- /dev/null +++ b/tools/build_check.cpp @@ -0,0 +1,1048 @@ +// tools/build_check.cpp - Fast native C++ build helper tool for Juliet project +// Replaces all PowerShell scripts (build_helper.ps1, check_target.ps1, clean_unity.ps1, summary_helper.ps1) +#ifndef NOMINMAX +#define NOMINMAX +#endif +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +#include +#include +#include +#include + +// ============================================================================ +// Minimal string / path helpers (no STL, no std::filesystem — pure speed) +// ============================================================================ + +static constexpr int MaxPath = 1024; +static constexpr int MaxFiles = 4096; +static constexpr int MaxHashEntries = 512; + +struct FixedString +{ + char Data[MaxPath]; + int Length; + + FixedString() : Data{}, Length(0) {} + + void Set(const char* str) + { + assert(str != nullptr); + Length = 0; + while (str[Length] != '\0' && Length < MaxPath - 1) + { + Data[Length] = str[Length]; + Length++; + } + Data[Length] = '\0'; + } + + void Set(const char* str, int len) + { + assert(str != nullptr); + assert(len >= 0 && len < MaxPath); + for (int i = 0; i < len; i++) + { + Data[i] = str[i]; + } + Data[len] = '\0'; + Length = len; + } + + void Append(const char* str) + { + assert(str != nullptr); + int i = 0; + while (str[i] != '\0' && Length < MaxPath - 1) + { + Data[Length++] = str[i++]; + } + Data[Length] = '\0'; + } + + void AppendChar(char c) + { + if (Length < MaxPath - 1) + { + Data[Length++] = c; + Data[Length] = '\0'; + } + } + + [[nodiscard]] bool Equals(const char* other) const + { + assert(other != nullptr); + return strcmp(Data, other) == 0; + } + + [[nodiscard]] bool EqualsNoCase(const char* other) const + { + assert(other != nullptr); + return _stricmp(Data, other) == 0; + } +}; + +[[nodiscard]] static bool StringContains(const char* haystack, const char* needle) +{ + assert(haystack != nullptr); + assert(needle != nullptr); + return strstr(haystack, needle) != nullptr; +} + +[[nodiscard]] static bool EndsWithNoCase(const char* str, const char* suffix) +{ + assert(str != nullptr); + assert(suffix != nullptr); + int strLen = static_cast(strlen(str)); + int sufLen = static_cast(strlen(suffix)); + if (sufLen > strLen) + { + return false; + } + return _stricmp(str + strLen - sufLen, suffix) == 0; +} + +// ============================================================================ +// File I/O helpers using Win32 +// ============================================================================ + +struct FileContent +{ + char* Data; + DWORD Size; +}; + +[[nodiscard]] static FileContent ReadEntireFile(const char* path) +{ + assert(path != nullptr); + FileContent result = {nullptr, 0}; + HANDLE file = CreateFileA(path, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); + if (file == INVALID_HANDLE_VALUE) + { + return result; + } + DWORD size = GetFileSize(file, nullptr); + if (size == INVALID_FILE_SIZE || size == 0) + { + CloseHandle(file); + return result; + } + char* buffer = static_cast(HeapAlloc(GetProcessHeap(), 0, size + 1)); + if (buffer == nullptr) + { + CloseHandle(file); + return result; + } + DWORD bytesRead = 0; + ReadFile(file, buffer, size, &bytesRead, nullptr); + CloseHandle(file); + buffer[bytesRead] = '\0'; + result.Data = buffer; + result.Size = bytesRead; + return result; +} + +static void FreeFileContent(FileContent& content) +{ + if (content.Data != nullptr) + { + HeapFree(GetProcessHeap(), 0, content.Data); + content.Data = nullptr; + content.Size = 0; + } +} + +static bool WriteEntireFile(const char* path, const char* data, DWORD size) +{ + assert(path != nullptr); + assert(data != nullptr); + HANDLE file = CreateFileA(path, GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); + if (file == INVALID_HANDLE_VALUE) + { + return false; + } + DWORD written = 0; + WriteFile(file, data, size, &written, nullptr); + CloseHandle(file); + return written == size; +} + +[[nodiscard]] static bool FileExists(const char* path) +{ + assert(path != nullptr); + DWORD attribs = GetFileAttributesA(path); + return (attribs != INVALID_FILE_ATTRIBUTES) && !(attribs & FILE_ATTRIBUTE_DIRECTORY); +} + +[[nodiscard]] static bool DirectoryExists(const char* path) +{ + assert(path != nullptr); + DWORD attribs = GetFileAttributesA(path); + return (attribs != INVALID_FILE_ATTRIBUTES) && (attribs & FILE_ATTRIBUTE_DIRECTORY); +} + +static void EnsureDirectoryExists(const char* path) +{ + assert(path != nullptr); + CreateDirectoryA(path, nullptr); +} + +[[nodiscard]] static int64_t GetFileModTimeInt64(const char* path) +{ + assert(path != nullptr); + WIN32_FILE_ATTRIBUTE_DATA data; + if (GetFileAttributesExA(path, GetFileExInfoStandard, &data)) + { + ULARGE_INTEGER li; + li.LowPart = data.ftLastWriteTime.dwLowDateTime; + li.HighPart = data.ftLastWriteTime.dwHighDateTime; + return static_cast(li.QuadPart); + } + return 0; +} + +// ============================================================================ +// FNV-1a hash +// ============================================================================ + +[[nodiscard]] static uint64_t ComputeFnv1aHash(const char* data, DWORD size) +{ + assert(data != nullptr); + uint64_t hash = 14695981039346656037ULL; + for (DWORD i = 0; i < size; i++) + { + hash ^= static_cast(static_cast(data[i])); + hash *= 1099511628211ULL; + } + return hash; +} + +// ============================================================================ +// Hash cache for unity files +// ============================================================================ + +struct HashEntry +{ + FixedString Path; + char HashStr[32]; +}; + +struct HashCache +{ + HashEntry* Entries; + int Count; + + HashCache() + { + Entries = static_cast(HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(HashEntry) * MaxHashEntries)); + assert(Entries != nullptr); + Count = 0; + } + + ~HashCache() + { + if (Entries != nullptr) + { + HeapFree(GetProcessHeap(), 0, Entries); + Entries = nullptr; + } + } + + HashCache(const HashCache&) = delete; + HashCache& operator=(const HashCache&) = delete; + + [[nodiscard]] const char* Find(const char* path) const + { + assert(path != nullptr); + for (int i = 0; i < Count; i++) + { + if (Entries[i].Path.Equals(path)) + { + return Entries[i].HashStr; + } + } + return nullptr; + } + + void Add(const char* path, const char* hash) + { + assert(path != nullptr); + assert(hash != nullptr); + if (Count < MaxHashEntries && Entries != nullptr) + { + Entries[Count].Path.Set(path); + strncpy_s(Entries[Count].HashStr, hash, sizeof(Entries[Count].HashStr) - 1); + Count++; + } + } +}; + +static void LoadHashCache(const char* cachePath, HashCache& cache) +{ + assert(cachePath != nullptr); + FileContent content = ReadEntireFile(cachePath); + if (content.Data == nullptr) + { + return; + } + + const char* pos = content.Data; + while (*pos != '\0') + { + const char* lineStart = pos; + while (*pos != '\0' && *pos != '\n' && *pos != '\r') + { + pos++; + } + int lineLen = static_cast(pos - lineStart); + + if (lineLen > 0) + { + const char* sep = nullptr; + for (const char* c = lineStart; c < lineStart + lineLen; c++) + { + if (*c == '=') + { + sep = c; + break; + } + } + if (sep != nullptr) + { + FixedString key; + key.Set(lineStart, static_cast(sep - lineStart)); + + int valLen = static_cast((lineStart + lineLen) - (sep + 1)); + char val[32] = {}; + if (valLen > 0 && valLen < 32) + { + memcpy(val, sep + 1, static_cast(valLen)); + val[valLen] = '\0'; + } + cache.Add(key.Data, val); + } + } + + while (*pos == '\n' || *pos == '\r') + { + pos++; + } + } + + FreeFileContent(content); +} + +static void SaveHashCache(const char* cachePath, const HashCache& cache) +{ + assert(cachePath != nullptr); + size_t bufSize = static_cast(MaxHashEntries) * (MaxPath + 32 + 2); + char* buffer = static_cast(HeapAlloc(GetProcessHeap(), 0, bufSize)); + if (buffer == nullptr) + { + return; + } + int offset = 0; + + for (int i = 0; i < cache.Count; i++) + { + int written = sprintf_s(buffer + offset, bufSize - static_cast(offset), + "%s=%s\n", cache.Entries[i].Path.Data, cache.Entries[i].HashStr); + if (written > 0) + { + offset += written; + } + } + + WriteEntireFile(cachePath, buffer, static_cast(offset)); + HeapFree(GetProcessHeap(), 0, buffer); +} + +// ============================================================================ +// Clean #pragma message lines and compute hash for a unity file +// ============================================================================ + +struct CleanResult +{ + char HashStr[32]; + bool WasCleaned; +}; + +[[nodiscard]] static CleanResult CleanAndHashUnityFile(const char* filePath) +{ + assert(filePath != nullptr); + CleanResult result = {{}, false}; + + FileContent content = ReadEntireFile(filePath); + if (content.Data == nullptr) + { + result.HashStr[0] = '\0'; + return result; + } + + bool hasPragma = StringContains(content.Data, "#pragma message"); + + if (hasPragma) + { + // Filter out #pragma message lines + char* filtered = static_cast(HeapAlloc(GetProcessHeap(), 0, content.Size + 1)); + assert(filtered != nullptr); + DWORD filteredSize = 0; + + const char* pos = content.Data; + while (*pos != '\0') + { + const char* lineStart = pos; + while (*pos != '\0' && *pos != '\n') + { + pos++; + } + bool skipLine = false; + for (const char* c = lineStart; c < pos; c++) + { + if (c + 15 <= content.Data + content.Size && memcmp(c, "#pragma message", 15) == 0) + { + skipLine = true; + break; + } + } + if (!skipLine) + { + DWORD lineLen = static_cast(pos - lineStart); + memcpy(filtered + filteredSize, lineStart, lineLen); + filteredSize += lineLen; + if (*pos == '\n') + { + filtered[filteredSize++] = '\n'; + } + } + if (*pos == '\n') + { + pos++; + } + } + filtered[filteredSize] = '\0'; + + WriteEntireFile(filePath, filtered, filteredSize); + + uint64_t hashVal = ComputeFnv1aHash(filtered, filteredSize); + sprintf_s(result.HashStr, "%llu", hashVal); + result.WasCleaned = true; + + HeapFree(GetProcessHeap(), 0, filtered); + } + else + { + uint64_t hashVal = ComputeFnv1aHash(content.Data, content.Size); + sprintf_s(result.HashStr, "%llu", hashVal); + } + + FreeFileContent(content); + return result; +} + +// ============================================================================ +// Recursive directory scan for newest source file time +// ============================================================================ + +[[nodiscard]] static int64_t GetNewestSourceTimeRecursive(const char* dirPath) +{ + assert(dirPath != nullptr); + int64_t newest = 0; + + if (!DirectoryExists(dirPath)) + { + return newest; + } + + FixedString searchPath; + searchPath.Set(dirPath); + searchPath.Append("\\*"); + + WIN32_FIND_DATAA findData; + HANDLE hFind = FindFirstFileA(searchPath.Data, &findData); + if (hFind == INVALID_HANDLE_VALUE) + { + return newest; + } + + do + { + if (findData.cFileName[0] == '.' && + (findData.cFileName[1] == '\0' || (findData.cFileName[1] == '.' && findData.cFileName[2] == '\0'))) + { + continue; + } + + FixedString fullPath; + fullPath.Set(dirPath); + fullPath.AppendChar('\\'); + fullPath.Append(findData.cFileName); + + if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) + { + int64_t subNewest = GetNewestSourceTimeRecursive(fullPath.Data); + if (subNewest > newest) + { + newest = subNewest; + } + } + else + { + if (EndsWithNoCase(findData.cFileName, ".cpp") || + EndsWithNoCase(findData.cFileName, ".h") || + EndsWithNoCase(findData.cFileName, ".hpp") || + EndsWithNoCase(findData.cFileName, ".inl") || + EndsWithNoCase(findData.cFileName, ".c")) + { + ULARGE_INTEGER li; + li.LowPart = findData.ftLastWriteTime.dwLowDateTime; + li.HighPart = findData.ftLastWriteTime.dwHighDateTime; + int64_t ft = static_cast(li.QuadPart); + if (ft > newest) + { + newest = ft; + } + } + } + } while (FindNextFileA(hFind, &findData)); + + FindClose(hFind); + return newest; +} + +// ============================================================================ +// Collect unity files recursively from Intermediate/ +// ============================================================================ + +struct FilePathList +{ + FixedString* Paths; + int Count; + + FilePathList() + { + Paths = static_cast(HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(FixedString) * MaxFiles)); + assert(Paths != nullptr); + Count = 0; + } + + ~FilePathList() + { + if (Paths != nullptr) + { + HeapFree(GetProcessHeap(), 0, Paths); + Paths = nullptr; + } + } + + FilePathList(const FilePathList&) = delete; + FilePathList& operator=(const FilePathList&) = delete; + + void Add(const char* path) + { + assert(path != nullptr); + if (Count < MaxFiles && Paths != nullptr) + { + Paths[Count].Set(path); + Count++; + } + } +}; + +static void CollectUnityFiles(const char* dirPath, FilePathList& list) +{ + assert(dirPath != nullptr); + FixedString searchPath; + searchPath.Set(dirPath); + searchPath.Append("\\*"); + + WIN32_FIND_DATAA findData; + HANDLE hFind = FindFirstFileA(searchPath.Data, &findData); + if (hFind == INVALID_HANDLE_VALUE) + { + return; + } + + do + { + if (findData.cFileName[0] == '.' && + (findData.cFileName[1] == '\0' || (findData.cFileName[1] == '.' && findData.cFileName[2] == '\0'))) + { + continue; + } + + FixedString fullPath; + fullPath.Set(dirPath); + fullPath.AppendChar('\\'); + fullPath.Append(findData.cFileName); + + if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) + { + CollectUnityFiles(fullPath.Data, list); + } + else + { + if (StringContains(findData.cFileName, "_Unity") && EndsWithNoCase(findData.cFileName, ".cpp")) + { + list.Add(fullPath.Data); + } + } + } while (FindNextFileA(hFind, &findData)); + + FindClose(hFind); +} + +// ============================================================================ +// Commands +// ============================================================================ + +static int CommandStartBuild() +{ + EnsureDirectoryExists("Intermediate"); + + DeleteFileA("Intermediate\\step_start.tmp"); + DeleteFileA("Intermediate\\step_timings.tmp"); + + FILETIME ft; + GetSystemTimeAsFileTime(&ft); + ULARGE_INTEGER li; + li.LowPart = ft.dwLowDateTime; + li.HighPart = ft.dwHighDateTime; + + char buf[32]; + sprintf_s(buf, "%llu", li.QuadPart); + WriteEntireFile("Intermediate\\build_start.tmp", buf, static_cast(strlen(buf))); + return 0; +} + +static int CommandStartStep(int argc, char* argv[]) +{ + assert(argv != nullptr); + EnsureDirectoryExists("Intermediate"); + + const char* stepName = (argc > 2 && argv[2] != nullptr) ? argv[2] : "Unnamed Step"; + + FILETIME ft; + GetSystemTimeAsFileTime(&ft); + ULARGE_INTEGER li; + li.LowPart = ft.dwLowDateTime; + li.HighPart = ft.dwHighDateTime; + + char buf[MaxPath + 64]; + int len = sprintf_s(buf, "%s\n%llu\n", stepName, li.QuadPart); + if (len > 0) + { + WriteEntireFile("Intermediate\\step_start.tmp", buf, static_cast(len)); + } + return 0; +} + +static int CommandEndStep(int argc, char* argv[]) +{ + assert(argv != nullptr); + if (!FileExists("Intermediate\\step_start.tmp")) + { + return 0; + } + + FileContent content = ReadEntireFile("Intermediate\\step_start.tmp"); + if (content.Data == nullptr) + { + return 0; + } + + char stepName[MaxPath] = "Unnamed Step"; + uint64_t startTicks = 0; + + const char* line1End = strchr(content.Data, '\n'); + if (line1End != nullptr) + { + int nameLen = static_cast(line1End - content.Data); + if (nameLen > 0 && nameLen < MaxPath) + { + memcpy(stepName, content.Data, static_cast(nameLen)); + stepName[nameLen] = '\0'; + } + sscanf_s(line1End + 1, "%llu", &startTicks); + } + FreeFileContent(content); + DeleteFileA("Intermediate\\step_start.tmp"); + + if (argc > 2 && argv[2] != nullptr && argv[2][0] != '\0') + { + strncpy_s(stepName, argv[2], MaxPath - 1); + } + + FILETIME ftNow; + GetSystemTimeAsFileTime(&ftNow); + ULARGE_INTEGER nowLi; + nowLi.LowPart = ftNow.dwLowDateTime; + nowLi.HighPart = ftNow.dwHighDateTime; + + double elapsedSeconds = static_cast(nowLi.QuadPart - startTicks) / 10000000.0; + + char appendBuf[MaxPath + 64]; + int appendLen = sprintf_s(appendBuf, "%s|%.3fs\n", stepName, elapsedSeconds); + if (appendLen > 0) + { + FileContent existing = ReadEntireFile("Intermediate\\step_timings.tmp"); + DWORD newSize = static_cast(appendLen) + existing.Size; + char* combined = static_cast(HeapAlloc(GetProcessHeap(), 0, newSize + 1)); + if (combined != nullptr) + { + DWORD offset = 0; + if (existing.Data != nullptr && existing.Size > 0) + { + memcpy(combined, existing.Data, existing.Size); + offset = existing.Size; + } + memcpy(combined + offset, appendBuf, static_cast(appendLen)); + combined[newSize] = '\0'; + WriteEntireFile("Intermediate\\step_timings.tmp", combined, newSize); + HeapFree(GetProcessHeap(), 0, combined); + } + FreeFileContent(existing); + } + + return 0; +} + +static int CommandFinishBuild() +{ + // Compute elapsed time + const char* elapsedStr = "0.000s"; + char elapsedBuf[64] = {}; + + if (FileExists("Intermediate\\build_start.tmp")) + { + FileContent startContent = ReadEntireFile("Intermediate\\build_start.tmp"); + if (startContent.Data != nullptr) + { + uint64_t startTicks = 0; + sscanf_s(startContent.Data, "%llu", &startTicks); + FreeFileContent(startContent); + + FILETIME ftNow; + GetSystemTimeAsFileTime(&ftNow); + ULARGE_INTEGER nowLi; + nowLi.LowPart = ftNow.dwLowDateTime; + nowLi.HighPart = ftNow.dwHighDateTime; + + double elapsedSeconds = static_cast(nowLi.QuadPart - startTicks) / 10000000.0; + + if (elapsedSeconds < 60.0) + { + sprintf_s(elapsedBuf, "%.3fs", elapsedSeconds); + } + else + { + int totalMs = static_cast(elapsedSeconds * 1000.0); + int hours = totalMs / 3600000; + int minutes = (totalMs % 3600000) / 60000; + int seconds = (totalMs % 60000) / 1000; + int ms = totalMs % 1000; + sprintf_s(elapsedBuf, "%02d:%02d:%02d.%03d", hours, minutes, seconds, ms); + } + elapsedStr = elapsedBuf; + } + DeleteFileA("Intermediate\\build_start.tmp"); + } + + if (FileExists("Intermediate\\step_timings.tmp")) + { + FileContent timingsContent = ReadEntireFile("Intermediate\\step_timings.tmp"); + if (timingsContent.Data != nullptr && timingsContent.Size > 0) + { + printf("Step Timings:\n"); + const char* pos = timingsContent.Data; + while (*pos != '\0') + { + const char* lineStart = pos; + while (*pos != '\0' && *pos != '\n' && *pos != '\r') + { + pos++; + } + int lineLen = static_cast(pos - lineStart); + while (*pos == '\n' || *pos == '\r') + { + pos++; + } + + if (lineLen > 0) + { + const char* pipePos = nullptr; + for (const char* p = lineStart; p < lineStart + lineLen; p++) + { + if (*p == '|') + { + pipePos = p; + break; + } + } + + if (pipePos != nullptr) + { + char stepName[MaxPath] = {}; + int nameLen = static_cast(pipePos - lineStart); + if (nameLen < MaxPath) + { + memcpy(stepName, lineStart, static_cast(nameLen)); + stepName[nameLen] = '\0'; + } + + char durationStr[64] = {}; + int durLen = static_cast((lineStart + lineLen) - (pipePos + 1)); + if (durLen < 64) + { + memcpy(durationStr, pipePos + 1, static_cast(durLen)); + durationStr[durLen] = '\0'; + } + + printf(" - %-42s : %s\n", stepName, durationStr); + } + } + } + printf("------------------------------------------------------------------------------\n"); + FreeFileContent(timingsContent); + } + DeleteFileA("Intermediate\\step_timings.tmp"); + } + + printf("Total Duration : %s\n", elapsedStr); + printf("Output Binaries:\n"); + + if (FileExists("Intermediate\\built_outputs.tmp")) + { + FileContent outputsContent = ReadEntireFile("Intermediate\\built_outputs.tmp"); + bool anyPrinted = false; + + if (outputsContent.Data != nullptr) + { + const char* pos = outputsContent.Data; + while (*pos != '\0') + { + const char* lineStart = pos; + while (*pos != '\0' && *pos != '\n' && *pos != '\r') + { + pos++; + } + int lineLen = static_cast(pos - lineStart); + while (*pos == '\n' || *pos == '\r') + { + pos++; + } + + if (lineLen > 0) + { + char filePath[MaxPath]; + if (lineLen < MaxPath) + { + memcpy(filePath, lineStart, static_cast(lineLen)); + filePath[lineLen] = '\0'; + + if (FileExists(filePath)) + { + WIN32_FILE_ATTRIBUTE_DATA fileData; + if (GetFileAttributesExA(filePath, GetFileExInfoStandard, &fileData)) + { + ULARGE_INTEGER fileSize; + fileSize.LowPart = fileData.nFileSizeLow; + fileSize.HighPart = fileData.nFileSizeHigh; + double sizeMB = static_cast(fileSize.QuadPart) / (1024.0 * 1024.0); + printf(" - %s (%.2f MB)\n", filePath, sizeMB); + anyPrinted = true; + } + } + } + } + } + FreeFileContent(outputsContent); + } + + if (!anyPrinted) + { + printf(" - None built in this session (all requested targets up-to-date)\n"); + } + DeleteFileA("Intermediate\\built_outputs.tmp"); + } + else + { + printf(" - None built in this session (all requested targets up-to-date)\n"); + } + + fflush(stdout); + return 0; +} + +static int CommandEvaluate(int argc, char* argv[]) +{ + LARGE_INTEGER perfStart, perfEnd, perfFreq; + QueryPerformanceCounter(&perfStart); + QueryPerformanceFrequency(&perfFreq); + + const char* cfg = (argc > 2) ? argv[2] : "Debug"; + bool targetJuliet = (argc > 3) && argv[3][0] == '1'; + bool targetGame = (argc > 4) && argv[4][0] == '1'; + bool targetRomeo = (argc > 5) && argv[5][0] == '1'; + bool targetShader = (argc > 6) && argv[6][0] == '1'; + + EnsureDirectoryExists("Intermediate"); + + // 1. Process unity files: clean #pragma message, compute hashes, detect changes + HashCache oldCache; + LoadHashCache("Intermediate\\.unity_hashes_cache", oldCache); + + HashCache newCache; + bool unityChanged = false; + + FilePathList unityFiles; + CollectUnityFiles("Intermediate", unityFiles); + + if (unityFiles.Count == 0) + { + unityChanged = true; + } + else + { + for (int i = 0; i < unityFiles.Count; i++) + { + CleanResult cr = CleanAndHashUnityFile(unityFiles.Paths[i].Data); + newCache.Add(unityFiles.Paths[i].Data, cr.HashStr); + + const char* oldHash = oldCache.Find(unityFiles.Paths[i].Data); + if (oldHash == nullptr || strcmp(oldHash, cr.HashStr) != 0) + { + unityChanged = true; + } + } + } + + SaveHashCache("Intermediate\\.unity_hashes_cache", newCache); + + // 2. Scan source directory modification times once + int64_t julietTime = GetNewestSourceTimeRecursive("Juliet"); + int64_t gameTime = GetNewestSourceTimeRecursive("Game"); + int64_t imguiTime = GetNewestSourceTimeRecursive("External\\imgui"); + int64_t appTime = GetNewestSourceTimeRecursive("JulietApp"); + int64_t romeoTime = GetNewestSourceTimeRecursive("Romeo"); + int64_t shaderTime = GetNewestSourceTimeRecursive("JulietShaderCompiler"); + + auto MaxTime = [](int64_t a, int64_t b) -> int64_t { return (a > b) ? a : b; }; + + int64_t julietCombined = MaxTime(julietTime, imguiTime); + int64_t gameCombined = MaxTime(MaxTime(julietTime, gameTime), imguiTime); + int64_t appCombined = MaxTime(MaxTime(MaxTime(julietTime, gameTime), appTime), imguiTime); + int64_t romeoCombined = MaxTime(MaxTime(julietTime, romeoTime), imguiTime); + int64_t shaderCombined = MaxTime(MaxTime(julietTime, shaderTime), imguiTime); + + // Build paths for target binaries + char binDir[MaxPath]; + char intDir[MaxPath]; + sprintf_s(binDir, "bin\\x64-%s", cfg); + sprintf_s(intDir, "Intermediate\\x64-%s", cfg); + + auto IsUpToDate = [](const char* targetPath, int64_t newestSourceTime) -> bool + { + assert(targetPath != nullptr); + if (!FileExists(targetPath)) + { + return false; + } + return GetFileModTimeInt64(targetPath) >= newestSourceTime; + }; + + char path[MaxPath]; + + sprintf_s(path, "%s\\Juliet_Unity1.obj", intDir); + bool julietNeed = unityChanged || !IsUpToDate(path, julietCombined); + + sprintf_s(path, "%s\\ImGui_Unity1.obj", intDir); + bool imguiNeed = unityChanged || !IsUpToDate(path, imguiTime); + + bool gameNeed = false; + if (targetJuliet || targetGame) + { + sprintf_s(path, "%s\\Game.dll", binDir); + gameNeed = julietNeed || unityChanged || !IsUpToDate(path, gameCombined); + } + + bool julietAppNeed = false; + if (targetJuliet) + { + sprintf_s(path, "%s\\JulietApp.exe", binDir); + julietAppNeed = gameNeed || unityChanged || !IsUpToDate(path, appCombined); + } + + bool romeoNeed = false; + if (targetRomeo) + { + sprintf_s(path, "%s\\Romeo.exe", binDir); + romeoNeed = julietNeed || unityChanged || !IsUpToDate(path, romeoCombined); + } + + bool shaderNeed = false; + if (targetShader && DirectoryExists("Intermediate\\JulietShaderCompiler")) + { + sprintf_s(path, "%s\\JulietShaderCompiler.exe", binDir); + shaderNeed = julietNeed || unityChanged || !IsUpToDate(path, shaderCombined); + } + + QueryPerformanceCounter(&perfEnd); + double durationMs = static_cast(perfEnd.QuadPart - perfStart.QuadPart) * 1000.0 / static_cast(perfFreq.QuadPart); + printf("[PERF] Fast Native C++ EvaluateConfig: %.1f ms\n", durationMs); + + // Write results + char statusBuf[512]; + int statusLen = sprintf_s(statusBuf, + "FASTBUILD_UNITY_CHANGED=%d\n" + "JULIET_NEED_COMPILE=%d\n" + "IMGUI_NEED_COMPILE=%d\n" + "GAME_NEED_COMPILE=%d\n" + "JULIETAPP_NEED_COMPILE=%d\n" + "ROMEO_NEED_COMPILE=%d\n" + "SHADER_NEED_COMPILE=%d\n", + unityChanged ? 1 : 0, + julietNeed ? 1 : 0, + imguiNeed ? 1 : 0, + gameNeed ? 1 : 0, + julietAppNeed ? 1 : 0, + romeoNeed ? 1 : 0, + shaderNeed ? 1 : 0); + + WriteEntireFile("Intermediate\\config_status.tmp", statusBuf, static_cast(statusLen)); + fflush(stdout); + return 0; +} + +// ============================================================================ +// Main entry point +// ============================================================================ + +int main(int argc, char* argv[]) +{ + if (argc < 2) + { + printf("Usage: build_check [args...]\n"); + printf("Commands:\n"); + printf(" start_build Record build start timestamp\n"); + printf(" start_step Record start timestamp of a build step\n"); + printf(" end_step [] Record duration of current step\n"); + printf(" finish_build Print build summary (elapsed time, step timings, built binaries)\n"); + printf(" evaluate Evaluate rebuild needs\n"); + printf(" check_alive Returns 0 immediately to test execution\n"); + return 1; + } + + const char* cmd = argv[1]; + if (strcmp(cmd, "check_alive") == 0) return 0; + if (strcmp(cmd, "start_build") == 0) return CommandStartBuild(); + if (strcmp(cmd, "start_step") == 0) return CommandStartStep(argc, argv); + if (strcmp(cmd, "end_step") == 0) return CommandEndStep(argc, argv); + if (strcmp(cmd, "finish_build")== 0) return CommandFinishBuild(); + if (strcmp(cmd, "evaluate") == 0) return CommandEvaluate(argc, argv); + + printf("[ERROR] Unknown command: %s\n", cmd); + return 1; +}