Files

425 lines
14 KiB
PowerShell

[CmdletBinding()]
param(
[switch]$SkipCoverage,
[switch]$ExemptMiri,
[string]$MiriExemptionReason,
[switch]$ExemptFuzz,
[string]$FuzzExemptionReason,
[switch]$ExemptSanitizers,
[string]$SanitizersExemptionReason,
[switch]$ExemptNoPanic,
[string]$NoPanicExemptionReason,
[switch]$ExemptLoom,
[string]$LoomExemptionReason,
[ValidateSet('Debug', 'Release', 'ReleaseFast', 'ReleaseSize')]
[string]$VerificationConfiguration = 'ReleaseFast'
)
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest
$ProgressPreference = 'SilentlyContinue'
$workspaceRoot = Split-Path -Parent $PSScriptRoot
Push-Location -LiteralPath $workspaceRoot
$script:StepResults = [System.Collections.Generic.List[object]]::new()
function Resolve-CoverageTargetRoot {
$override = [Environment]::GetEnvironmentVariable('MERCURY_JADE_COVERAGE_ROOT')
if (-not [string]::IsNullOrWhiteSpace($override)) {
return $override
}
$shortScratch = 'C:\tmp'
if (Test-Path -LiteralPath $shortScratch -PathType Container) {
return (Join-Path $shortScratch 'mtcov')
}
return (Join-Path ([System.IO.Path]::GetTempPath()) 'mtcov')
}
$coverageTargetRoot = Resolve-CoverageTargetRoot
$coverageTargetDir = $null
function Add-StepResult {
param(
[Parameter(Mandatory = $true)]
[string]$Name,
[Parameter(Mandatory = $true)]
[double]$DurationSeconds,
[Parameter(Mandatory = $true)]
[string]$Mode
)
$script:StepResults.Add([pscustomobject]@{
Step = $Name
Seconds = [math]::Round($DurationSeconds, 2)
Mode = $Mode
})
}
function Resolve-BuildArguments {
param(
[Parameter(Mandatory = $true)]
[string]$Configuration
)
$arguments = @('build', '--workspace')
switch ($Configuration) {
'Release' {
$arguments += '--release'
}
'ReleaseFast' {
$arguments += @('--profile', 'release-fast')
}
'ReleaseSize' {
$arguments += @('--profile', 'release-size')
}
}
return $arguments
}
function Add-ExemptionArguments {
param(
[Parameter(Mandatory = $true)]
[System.Collections.Generic.List[string]]$Arguments,
[Parameter(Mandatory = $true)]
[string]$Name,
[Parameter(Mandatory = $true)]
[bool]$Requested,
[string]$Reason
)
if (-not $Requested) {
return
}
if ([string]::IsNullOrWhiteSpace($Reason)) {
throw "${Name} exemption requires a non-empty reason."
}
$Arguments.Add("-Exempt${Name}")
$Arguments.Add("-${Name}ExemptionReason")
$Arguments.Add($Reason)
}
function Invoke-TimedNative {
param(
[Parameter(Mandatory = $true)]
[string]$Name,
[Parameter(Mandatory = $true)]
[string]$FilePath,
[Parameter(Mandatory = $true)]
[string[]]$ArgumentList
)
$stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
& $FilePath @ArgumentList
$exitCode = $LASTEXITCODE
$stopwatch.Stop()
Add-StepResult -Name $Name -DurationSeconds $stopwatch.Elapsed.TotalSeconds -Mode 'sequential'
if ($exitCode -ne 0) {
throw "Command failed with exit code ${exitCode}: $FilePath $($ArgumentList -join ' ')"
}
}
function Invoke-TimedNativeWithEnvironment {
param(
[Parameter(Mandatory = $true)]
[string]$Name,
[Parameter(Mandatory = $true)]
[string]$FilePath,
[Parameter(Mandatory = $true)]
[string[]]$ArgumentList,
[Parameter(Mandatory = $true)]
[hashtable]$Environment
)
$previous = @{}
foreach ($key in $Environment.Keys) {
$item = Get-Item -LiteralPath "Env:$key" -ErrorAction SilentlyContinue
$previous[$key] = [pscustomobject]@{
Exists = $null -ne $item
Value = if ($null -ne $item) { $item.Value } else { $null }
}
Set-Item -LiteralPath "Env:$key" -Value ([string]$Environment[$key])
}
try {
Invoke-TimedNative -Name $Name -FilePath $FilePath -ArgumentList $ArgumentList
}
finally {
foreach ($key in $previous.Keys) {
if ($previous[$key].Exists) {
Set-Item -LiteralPath "Env:$key" -Value $previous[$key].Value
}
else {
Remove-Item -LiteralPath "Env:$key" -ErrorAction SilentlyContinue
}
}
}
}
function Invoke-TimedNativeToFile {
param(
[Parameter(Mandatory = $true)]
[string]$Name,
[Parameter(Mandatory = $true)]
[string]$FilePath,
[Parameter(Mandatory = $true)]
[string[]]$ArgumentList,
[Parameter(Mandatory = $true)]
[string]$OutputPath
)
$stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
& $FilePath @ArgumentList > $OutputPath
$exitCode = $LASTEXITCODE
$stopwatch.Stop()
Add-StepResult -Name $Name -DurationSeconds $stopwatch.Elapsed.TotalSeconds -Mode 'sequential'
if ($exitCode -ne 0) {
throw "Command failed with exit code ${exitCode}: $FilePath $($ArgumentList -join ' ')"
}
}
function Invoke-ParallelNativeSteps {
param(
[Parameter(Mandatory = $true)]
[System.Collections.IEnumerable]$Steps
)
$jobScript = {
param(
[string]$Name,
[string]$FilePath,
[string[]]$ArgumentList,
[int]$Order,
[string]$WorkingDirectory
)
$ProgressPreference = 'SilentlyContinue'
Set-Location -LiteralPath $WorkingDirectory
$outputPath = Join-Path ([System.IO.Path]::GetTempPath()) ("mercury-jade-" + [System.Guid]::NewGuid() + '.log')
$stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
$exitCode = 0
try {
& $FilePath @ArgumentList *> $outputPath
$exitCode = $LASTEXITCODE
}
catch {
$_ | Out-String | Set-Content -LiteralPath $outputPath -Encoding utf8NoBOM
$exitCode = if ($LASTEXITCODE -ne 0) { $LASTEXITCODE } else { 1 }
}
finally {
$stopwatch.Stop()
}
return [pscustomobject]@{
Name = $Name
FilePath = $FilePath
Arguments = $ArgumentList -join ' '
OutputPath = $outputPath
ExitCode = [int]$exitCode
Seconds = [math]::Round($stopwatch.Elapsed.TotalSeconds, 2)
Order = $Order
}
}
$jobs = [System.Collections.Generic.List[object]]::new()
$index = 0
foreach ($step in $Steps) {
$jobs.Add((Start-Job -ScriptBlock $jobScript -ArgumentList @(
[string]$step.Name,
[string]$step.FilePath,
[string[]]$step.ArgumentList,
$index,
$workspaceRoot
)))
$index += 1
}
Wait-Job -Job $jobs | Out-Null
$failures = [System.Collections.Generic.List[string]]::new()
foreach ($job in $jobs) {
$result = Receive-Job -Job $job
Remove-Job -Job $job -Force
$output = ''
if (Test-Path -LiteralPath $result.OutputPath) {
$output = Get-Content -Raw -LiteralPath $result.OutputPath
Remove-Item -LiteralPath $result.OutputPath -Force -ErrorAction SilentlyContinue
}
if (-not [string]::IsNullOrWhiteSpace($output)) {
Write-Host ''
Write-Host "[$($result.Name)]"
Write-Host $output.TrimEnd()
}
Add-StepResult -Name $result.Name -DurationSeconds $result.Seconds -Mode 'parallel'
if ($result.ExitCode -ne 0) {
$failures.Add("$($result.Name) (exit $($result.ExitCode))")
}
}
if ($failures.Count -gt 0) {
throw "Parallel step(s) failed: $($failures -join ', ')"
}
}
try {
Invoke-TimedNative -Name 'cargo fmt' -FilePath 'cargo' -ArgumentList @('fmt', '--all', '--check')
Invoke-TimedNative -Name 'cargo check' -FilePath 'cargo' -ArgumentList @('check', '--all-targets', '--all-features')
if ($SkipCoverage) {
Invoke-TimedNative -Name 'cargo nextest' -FilePath 'cargo' -ArgumentList @('nextest', 'run', '--all-features')
} else {
$coverageTargetDir = Join-Path $coverageTargetRoot ("mercury-jade-llvm-cov-" + [System.Guid]::NewGuid().ToString('N').Substring(0, 8))
$coverageEnvironment = @{
CARGO_INCREMENTAL = '0'
RUSTC_WRAPPER = ''
CARGO_TARGET_DIR = $coverageTargetDir
}
Invoke-TimedNativeWithEnvironment -Name 'cargo llvm-cov clean' -FilePath 'cargo' -Environment $coverageEnvironment -ArgumentList @('llvm-cov', 'clean', '--workspace')
Invoke-TimedNativeWithEnvironment -Name 'cargo llvm-cov nextest' -FilePath 'cargo' -Environment $coverageEnvironment -ArgumentList @(
'llvm-cov',
'--jobs',
'1',
'nextest',
'--all-features',
'--summary-only',
'--ignore-filename-regex',
'cli\.rs$|main\.rs$|flow_opcode_table\.rs$',
'--fail-under-lines',
'80'
)
}
Invoke-TimedNative -Name 'cargo clippy' -FilePath 'cargo' -ArgumentList @(
'clippy',
'--all-targets',
'--all-features',
'--',
'-D',
'warnings',
'-W',
'clippy::pedantic',
'-W',
'clippy::nursery'
)
Invoke-TimedNative -Name 'cargo udeps' -FilePath 'cargo' -ArgumentList @('+nightly', 'udeps', '--all-targets', '--all-features')
Invoke-TimedNative -Name 'cargo deny' -FilePath 'cargo' -ArgumentList @('deny', 'check')
$fuzzManifest = Join-Path $workspaceRoot 'fuzz/Cargo.toml'
if (Test-Path -LiteralPath $fuzzManifest) {
Invoke-TimedNative -Name 'cargo fuzz udeps' -FilePath 'cargo' -ArgumentList @(
'+nightly',
'udeps',
'--manifest-path',
$fuzzManifest,
'--all-targets',
'--all-features'
)
$fuzzMetadataPath = Join-Path ([System.IO.Path]::GetTempPath()) ("mercury-fuzz-metadata-" + [System.Guid]::NewGuid() + '.json')
try {
Invoke-TimedNativeToFile -Name 'cargo fuzz metadata' -FilePath 'cargo' -ArgumentList @(
'metadata',
'--manifest-path',
$fuzzManifest,
'--locked',
'--format-version',
'1'
) -OutputPath $fuzzMetadataPath
Invoke-TimedNative -Name 'cargo fuzz deny' -FilePath 'cargo' -ArgumentList @(
'deny',
'check',
'--config',
(Join-Path $workspaceRoot 'deny.toml'),
'--metadata-path',
$fuzzMetadataPath
)
}
finally {
Remove-Item -LiteralPath $fuzzMetadataPath -Force -ErrorAction SilentlyContinue
}
} else {
throw "Fuzz workspace manifest is required by Jade and was not found: $fuzzManifest"
}
$jadeHardeningArguments = [System.Collections.Generic.List[string]]::new()
$jadeHardeningArguments.Add('-NoProfile')
$jadeHardeningArguments.Add('-File')
$jadeHardeningArguments.Add((Join-Path $PSScriptRoot 'check-jade-hardening.ps1'))
Add-ExemptionArguments -Arguments $jadeHardeningArguments -Name 'Miri' -Requested $ExemptMiri.IsPresent -Reason $MiriExemptionReason
Add-ExemptionArguments -Arguments $jadeHardeningArguments -Name 'Fuzz' -Requested $ExemptFuzz.IsPresent -Reason $FuzzExemptionReason
Add-ExemptionArguments -Arguments $jadeHardeningArguments -Name 'Sanitizers' -Requested $ExemptSanitizers.IsPresent -Reason $SanitizersExemptionReason
Add-ExemptionArguments -Arguments $jadeHardeningArguments -Name 'NoPanic' -Requested $ExemptNoPanic.IsPresent -Reason $NoPanicExemptionReason
Add-ExemptionArguments -Arguments $jadeHardeningArguments -Name 'Loom' -Requested $ExemptLoom.IsPresent -Reason $LoomExemptionReason
Invoke-TimedNative -Name 'jade hardening' -FilePath 'pwsh' -ArgumentList $jadeHardeningArguments.ToArray()
Invoke-TimedNative -Name "cargo build ($VerificationConfiguration)" -FilePath 'cargo' -ArgumentList (Resolve-BuildArguments -Configuration $VerificationConfiguration)
Invoke-ParallelNativeSteps -Steps @(
@{
Name = 'check-powershell'
FilePath = 'pwsh'
ArgumentList = @('-NoProfile', '-File', (Join-Path $PSScriptRoot 'check-powershell.ps1'))
},
@{
Name = 'check-ai-prompt'
FilePath = 'pwsh'
ArgumentList = @(
'-NoProfile',
'-File',
(Join-Path $PSScriptRoot 'check-ai-prompt.ps1'),
'-Configuration',
$VerificationConfiguration,
'-SkipBuild'
)
},
@{
Name = 'check-ai-skill'
FilePath = 'pwsh'
ArgumentList = @(
'-NoProfile',
'-File',
(Join-Path $PSScriptRoot 'check-ai-skill.ps1'),
'-Configuration',
$VerificationConfiguration,
'-SkipBuild'
)
}
)
Invoke-TimedNative -Name 'check-ecosystem' -FilePath 'pwsh' -ArgumentList @(
'-NoProfile',
'-File',
(Join-Path $PSScriptRoot 'check-ecosystem.ps1'),
'-Configuration',
$VerificationConfiguration,
'-SkipBuild',
'-SkipPromptGeneration'
)
Write-Host ''
$script:StepResults |
Sort-Object Step |
Format-Table -AutoSize
}
finally {
if ($null -ne $coverageTargetDir -and (Test-Path -LiteralPath $coverageTargetDir)) {
$resolvedRoot = [System.IO.Path]::GetFullPath($coverageTargetRoot)
$resolvedTarget = [System.IO.Path]::GetFullPath($coverageTargetDir)
$rootPrefix = $resolvedRoot.TrimEnd([System.IO.Path]::DirectorySeparatorChar, [System.IO.Path]::AltDirectorySeparatorChar) + [System.IO.Path]::DirectorySeparatorChar
if ($resolvedTarget.StartsWith($rootPrefix, [System.StringComparison]::OrdinalIgnoreCase)) {
Remove-Item -LiteralPath $coverageTargetDir -Recurse -Force -ErrorAction SilentlyContinue
}
}
Pop-Location
}