425 lines
14 KiB
PowerShell
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
|
|
}
|