Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,22 @@ jobs:
Build\Agent\TokenHygiene.Tests.ps1
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }

# The native build runs this staleness check under Windows PowerShell 5.1;
# FwBuildHelpers.psm1 itself loads under either engine.
- name: Native object staleness tests (PowerShell 7)
id: native-obj-staleness-tests-pwsh
shell: pwsh
run: |
Build/Agent/NativeObjStaleness.Tests.ps1
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }

- name: Native object staleness tests (Windows PowerShell 5.1)
id: native-obj-staleness-tests-winps
shell: powershell
run: |
Build\Agent\NativeObjStaleness.Tests.ps1
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }

# -TokenHygiene fails CI on any hardcoded color/spacing literal in the Avalonia
# surface (full-tree, no grandfathering -- unlike -CommentHygiene, which stays
# advisory-only for humans here and only blocks agents locally).
Expand Down
144 changes: 144 additions & 0 deletions Build/Agent/FwBuildHelpers.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -601,6 +601,148 @@ function Test-ViewsNativeArtifactsStale {
return $latestSource -gt $oldestArtifact
}

function Get-NativeMakefileModules {
<#
.SYNOPSIS
Returns the nmake-built native modules with the directories whose headers each one
compiles against.
.DESCRIPTION
Each entry names the module's folder under Obj\<Configuration> and the repo-relative
directories, scanned without recursion, whose headers and makefiles feed its
objects. InputDirs covers the module makefile's UI include list, the folders its
sources reach through path-qualified includes, and the shared Include and Bld
folders. GeneratedDirs holds build-output folders as format strings taking the
configuration name; they exist only after a build. A directory missing from either
list leaves edits to its headers undetected for that module.
#>
$shared = @('Include', 'Include\unicode', 'Bld')
$generated = @('Output\{0}\Common')
return @(
[pscustomobject]@{
Name = 'Views'
InputDirs = @('Src\views', 'Src\views\lib', 'Src\Generic', 'Src\AppCore',
'Src\AppCore\Res', 'Src\Cellar', 'Src\DebugProcs', 'Src\Kernel',
'Lib\src\graphite2\include\graphite2') + $shared
GeneratedDirs = $generated
},
[pscustomobject]@{
Name = 'FwKernel'
InputDirs = @('Src\Kernel', 'Src\Generic', 'Src\AppCore', 'Src\DebugProcs',
'Src\Cellar') + $shared
GeneratedDirs = $generated
},
[pscustomobject]@{
Name = 'Generic'
InputDirs = @('Src\Generic') + $shared
GeneratedDirs = $generated
},
[pscustomobject]@{
Name = 'DebugProcs'
InputDirs = @('Src\DebugProcs') + $shared
GeneratedDirs = @()
}
)
}

function Remove-NativeObjDirsWithStaleInputs {
<#
.SYNOPSIS
Deletes a native module's intermediate folder when any header or makefile it builds
from is newer than its oldest object file.
.DESCRIPTION
The nmake rules in Bld rebuild an object only when its own source file changes, so a
header edit leaves every unedited file that includes it compiled against the old
header. When a class changes size that way, objects disagree about its layout and the
program corrupts memory at runtime. Deleting the module's Obj folder makes the next
native build compile all of it.

The check is coarse by design: any newer input rebuilds the whole module, including
files that never include the changed header.
.PARAMETER Modules
Module entries shaped like Get-NativeMakefileModules output. Defaults to that list.
.OUTPUTS
The names of the modules whose folders were removed.
#>
param(
[Parameter(Mandatory)][string]$RepoRoot,
[Parameter(Mandatory)][string]$Configuration,
[object[]]$Modules = (Get-NativeMakefileModules)
)

$inputPatterns = @('*.h', '*.hpp', '*.inl', '*.idh', '*.mak')
# bldinc.h holds only version stamps that local builds regenerate daily; a stale copy in
# an object changes no layout.
$ignoredInputs = @('bldinc.h')
$removed = New-Object System.Collections.Generic.List[string]

foreach ($module in $Modules) {
$objDir = Join-Path $RepoRoot (Join-Path "Obj\$Configuration" $module.Name)
if (-not (Test-Path -LiteralPath $objDir -PathType Container)) {
continue
}

$oldestObj = $null
foreach ($obj in ([System.IO.DirectoryInfo]$objDir).EnumerateFiles('*.obj', [System.IO.SearchOption]::AllDirectories)) {
if ($null -eq $oldestObj -or $obj.LastWriteTimeUtc -lt $oldestObj.LastWriteTimeUtc) {
$oldestObj = $obj
}
}
if ($null -eq $oldestObj) {
continue
}

$relativeDirs = @($module.InputDirs)
if ($module.PSObject.Properties['GeneratedDirs']) {
foreach ($format in $module.GeneratedDirs) {
$relativeDirs += ($format -f $Configuration)
}
}

$newestInput = $null
foreach ($relativeDir in $relativeDirs) {
$inputDir = Join-Path $RepoRoot $relativeDir
if (-not (Test-Path -LiteralPath $inputDir -PathType Container)) {
continue
}
foreach ($pattern in $inputPatterns) {
foreach ($file in ([System.IO.DirectoryInfo]$inputDir).EnumerateFiles($pattern, [System.IO.SearchOption]::TopDirectoryOnly)) {
if ($ignoredInputs -contains $file.Name) {
continue
}
if ($null -eq $newestInput -or $file.LastWriteTimeUtc -gt $newestInput.LastWriteTimeUtc) {
$newestInput = $file
}
}
}
}

if ($null -eq $newestInput -or $newestInput.LastWriteTimeUtc -le $oldestObj.LastWriteTimeUtc) {
continue
}

$inputLabel = $newestInput.FullName.Substring($RepoRoot.TrimEnd('\').Length + 1)
$objLabel = $oldestObj.FullName.Substring($objDir.TrimEnd('\').Length + 1)
Write-Host ("[INFO] {0}: {1} ({2:yyyy-MM-dd HH:mm}) is newer than {3} ({4:yyyy-MM-dd HH:mm}); removing Obj\{5}\{0} so it rebuilds completely." -f `
$module.Name, $inputLabel, $newestInput.LastWriteTime, $objLabel, $oldestObj.LastWriteTime, $Configuration) -ForegroundColor Yellow
$deleteErrors = @()
Remove-Item -LiteralPath $objDir -Recurse -Force -ErrorAction SilentlyContinue -ErrorVariable deleteErrors
if (Test-Path -LiteralPath $objDir) {
# The compiler's program-database server can hold a .pdb open between builds, and a
# leftover debug database does not affect what gets compiled.
$blocking = @(([System.IO.DirectoryInfo]$objDir).EnumerateFiles('*', [System.IO.SearchOption]::AllDirectories) |
Where-Object { $_.Extension -notin @('.pdb', '.idb') })
if ($blocking.Count -gt 0) {
$reason = if ($deleteErrors.Count -gt 0) { $deleteErrors[0].Exception.Message } else { 'unknown error' }
throw "Could not remove $($blocking[0].FullName) while clearing stale native objects: $reason"
}
Write-Host "[WARN] $($module.Name): a debug database in Obj\$Configuration\$($module.Name) is in use and was left in place; every object was removed, so the module still rebuilds completely." -ForegroundColor Yellow
}
$removed.Add($module.Name)
}

return ,$removed.ToArray()
}

function Set-TestAssertDialogEnvironment {
<#!
.SYNOPSIS
Expand Down Expand Up @@ -765,6 +907,8 @@ Export-ModuleMember -Function @(
'Get-NewestWriteTimeUtc',
'Get-OldestWriteTimeUtc',
'Test-ViewsNativeArtifactsStale',
'Get-NativeMakefileModules',
'Remove-NativeObjDirsWithStaleInputs',
'Set-TestAssertDialogEnvironment',
'Disable-CrashDialog',
'Get-UnitppSummary',
Expand Down
Loading
Loading