Files
ARCADIA-Studio/harness/experiments/text-diagnostics-20260913/notices/scripts/windows-notices.ps1
T
sorokin.oleg.edu 9fd76aef3a Add current-draft diagnostic underlines and passive hover
Preserve raw include diagnostic provenance and derive bands from the existing shaped editor. Retain deferred tooltip paragraphs through rendering. Include source, regression, failure and native-trial evidence; corrected native acceptance remains pending.
2026-09-13 01:43:22 -07:00

212 lines
16 KiB
PowerShell

#Requires -Version 5.1
# Read-only dependency preflight. Does not build, download, or change a Cargo target.
function Get-ArcadiaNotices([string]$Repository, [string]$Target, [string]$NoticeManifest, [string]$LlvmMinGWRoot, [string[]]$RootPackages = @('oa-desktop', 'oa-server')) {
if ($RootPackages.Count -eq 0 -or @($RootPackages | Where-Object { $_ -notin @('oa-desktop', 'oa-server', 'oa-cli') }).Count -gt 0) { throw 'Notice roots must be oa-desktop, oa-server and/or oa-cli.' }
if ($LlvmMinGWRoot -and $Target -ne 'x86_64-pc-windows-gnullvm') { throw 'LLVM-MinGW runtime notices apply only to the gnullvm target.' }
function Hash-File([string]$Path) {
$stream = [IO.File]::OpenRead($Path)
$sha = [Security.Cryptography.SHA256]::Create()
try { return [BitConverter]::ToString($sha.ComputeHash($stream)).Replace('-', '').ToLowerInvariant() }
finally { $sha.Dispose(); $stream.Dispose() }
}
function Under-Root([string]$Root, [string]$Relative) {
$base = [IO.Path]::GetFullPath($Root).TrimEnd('\', '/') + [IO.Path]::DirectorySeparatorChar
$path = [IO.Path]::GetFullPath((Join-Path $base $Relative))
$comparison = if ([IO.Path]::DirectorySeparatorChar -eq '/') { [StringComparison]::Ordinal } else { [StringComparison]::OrdinalIgnoreCase }
if (-not $path.StartsWith($base, $comparison)) { throw "Notice path escapes source root: $Relative" }
if (-not [IO.File]::Exists($path)) { throw "Required notice/source missing: $path" }
return $path
}
$inputs = [ordered]@{}
$hashes = @{}
$failures = New-Object 'Collections.Generic.List[string]'
$records = New-Object 'Collections.Generic.List[object]'
$supplements = @{ packages = @(); assets = @(); runtimes = @() }
if (Test-Path -LiteralPath $NoticeManifest -PathType Leaf) { $supplements = Get-Content -LiteralPath $NoticeManifest -Raw | ConvertFrom-Json }
$supplementRoot = Split-Path -Parent ([IO.Path]::GetFullPath($NoticeManifest))
function Add-Notice([string]$Source, [string]$Destination) {
if ($inputs.Contains($Destination) -and $inputs[$Destination] -ne $Source) { throw "Duplicate notice destination: $Destination" }
$inputs[$Destination] = $Source
$hashes[$Destination] = Hash-File $Source
return [ordered]@{ path = $Destination; sha256 = $hashes[$Destination] }
}
function Add-Supplements($Entries, [string]$Prefix) {
foreach ($entry in $Entries) {
if ($entry.sha256 -notmatch '^[a-f0-9]{64}$' -or $entry.url -notmatch '^https://') { throw 'Supplement requires SHA-256 and an HTTPS provenance URL.' }
$source = Under-Root $supplementRoot $entry.file
if ((Hash-File $source) -ne $entry.sha256) { throw "Supplement changed: $($entry.file)" }
$record = Add-Notice $source "$Prefix/$([IO.Path]::GetFileName($source))"
$record['origin'] = $entry.url
$record
}
}
$lock = Under-Root $Repository 'Cargo.lock'
$lockHash = Hash-File $lock
# Shared compiler inputs live outside individual crate directories.
$workspaceInputs = [ordered]@{}
foreach ($relative in @('Cargo.toml', 'Cargo.lock', 'LICENSE', 'docs/openapi.json')) {
$workspaceInputs[$relative] = Hash-File (Under-Root $Repository $relative)
}
foreach ($file in @(Get-ChildItem -LiteralPath (Join-Path $Repository 'build') -Recurse -File | Sort-Object FullName)) {
if (($file.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) { throw 'Shared build input reparse point requires review.' }
$relative = $file.FullName.Substring([IO.Path]::GetFullPath($Repository).TrimEnd('\', '/').Length + 1).Replace('\', '/')
$workspaceInputs[$relative] = Hash-File $file.FullName
}
if (-not $workspaceInputs.Contains('build/windows_manifest.rs')) { throw 'Shared Windows build manifest input is missing.' }
$locked = @{}
foreach ($block in ([IO.File]::ReadAllText($lock) -split '(?m)^\[\[package\]\]\s*$')) {
$fields = @{}
foreach ($field in [regex]::Matches($block, '(?m)^(name|version|source|checksum) = "([^"]+)"\s*$')) { $fields[$field.Groups[1].Value] = $field.Groups[2].Value }
if ($fields.ContainsKey('source')) { $locked["$($fields.name)|$($fields.version)|$($fields.source)"] = $fields }
}
$raw = @(& cargo metadata --locked --offline --manifest-path (Join-Path $Repository 'Cargo.toml') --filter-platform $Target --format-version 1)
if ($LASTEXITCODE -ne 0) { throw 'Locked offline Cargo metadata failed; prepare the matching dependency cache first.' }
$metadata = ($raw -join "`n") | ConvertFrom-Json
if ((Hash-File $lock) -ne $lockHash) { throw 'Cargo.lock changed during notice preflight.' }
$packages = @{}; $nodes = @{}
foreach ($package in $metadata.packages) { $packages[$package.id] = $package }
foreach ($node in $metadata.resolve.nodes) { $nodes[$node.id] = $node }
$queue = New-Object 'Collections.Generic.Queue[string]'
foreach ($name in @($RootPackages | Sort-Object -Unique)) {
$roots = @($metadata.packages | Where-Object { $_.name -eq $name -and $metadata.workspace_members -contains $_.id })
if ($roots.Count -ne 1) { throw "Expected one workspace package: $name" }
$queue.Enqueue($roots[0].id)
}
$selected = New-Object 'Collections.Generic.HashSet[string]'
while ($queue.Count -gt 0) {
$id = $queue.Dequeue()
if (-not $selected.Add($id)) { continue }
if (-not $nodes.ContainsKey($id)) { throw "Unresolved dependency node: $id" }
foreach ($dep in $nodes[$id].deps) {
if (@($dep.dep_kinds | Where-Object { $_.kind -ne 'dev' }).Count -gt 0) { $queue.Enqueue($dep.pkg) }
}
}
foreach ($id in @($selected | Sort-Object)) {
$package = $packages[$id]
$root = [IO.Path]::GetFullPath((Split-Path -Parent $package.manifest_path)).TrimEnd('\', '/')
$isWorkspace = $metadata.workspace_members -contains $id
$localPath = $null
if (-not $package.source) {
$repositoryRoot = [IO.Path]::GetFullPath($Repository).TrimEnd('\', '/')
$repositoryPrefix = $repositoryRoot + [IO.Path]::DirectorySeparatorChar
$manifestPath = [IO.Path]::GetFullPath($package.manifest_path)
$comparison = if ([IO.Path]::DirectorySeparatorChar -eq '/') { [StringComparison]::Ordinal } else { [StringComparison]::OrdinalIgnoreCase }
if (-not $manifestPath.StartsWith($repositoryPrefix, $comparison)) { throw "Local dependency is outside repository: $id" }
$current = Get-Item -LiteralPath $manifestPath
while ($null -ne $current) {
if (($current.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) { throw "Local dependency has a reparse ancestor: $id" }
if ($current.FullName.Equals($repositoryRoot, $comparison)) { break }
$current = if ($current -is [IO.FileInfo]) { $current.Directory } else { $current.Parent }
}
$localPath = if ($root.Equals($repositoryRoot, $comparison)) { '.' } else { $root.Substring($repositoryPrefix.Length).Replace('\', '/') }
}
$key = "$($package.name)-$($package.version)"
if ($key -notmatch '^[A-Za-z0-9._+-]+$') { throw "Unsafe package identity: $key" }
$prefix = "licenses/dependencies/$key"
$registryChecksum = $null
if ($package.source) {
if ($package.source -notlike 'registry+*') { $failures.Add("Unsupported source requiring deliberate review: $id"); continue }
$lockKey = "$($package.name)|$($package.version)|$($package.source)"
if (-not $locked.ContainsKey($lockKey)) { throw "Source is absent from Cargo.lock: $id" }
$registryChecksum = $locked[$lockKey].checksum
if ($registryChecksum -notmatch '^[a-f0-9]{64}$') { throw "Missing registry checksum: $key" }
$registryRoot = Split-Path -Parent (Split-Path -Parent (Split-Path -Parent $root))
$archivePath = Under-Root $registryRoot "cache/$([IO.Path]::GetFileName((Split-Path -Parent $root)))/$key.crate"
if ((Hash-File $archivePath) -ne $registryChecksum) { throw "Cached crate archive differs from Cargo.lock: $key" }
}
# Hash actual local files (not merely Cargo's cached checksum claims).
$sourceHashes = [ordered]@{}
$notices = New-Object 'Collections.Generic.List[object]'
$hasLicenseText = $false
$fontReferences = New-Object 'Collections.Generic.HashSet[string]'
foreach ($file in @(Get-ChildItem -LiteralPath $root -Recurse -File | Sort-Object FullName)) {
if (($file.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) { throw "Source reparse point requires review: $($file.FullName)" }
$relative = $file.FullName.Substring($root.Length + 1).Replace('\', '/')
if ($relative -match '^target/' -or $file.Name -eq '.cargo-ok') { continue }
$sourceHashes[$relative] = Hash-File $file.FullName
if ($file.Name -match '(^|[-._])(LICENSE|LICENCE|COPYING|NOTICE|OFL|UNLICENSE|COPYRIGHT)([-._]|$)') {
$notices.Add((Add-Notice $file.FullName "$prefix/$relative"))
}
if ($file.Name -match '(^|[-._])(LICENSE|LICENCE|COPYING|OFL|UNLICENSE)([-._]|$)') { $hasLicenseText = $true }
if (-not $package.source -and -not $isWorkspace -and $relative -eq 'ARCADIA-PATCH.md') {
$notices.Add((Add-Notice $file.FullName "$prefix/$relative"))
}
# Literal embedded fonts in production/build source, including feature-gated ones
# conservatively. Tests/examples/benches are excluded, never all font files by glob.
if ($file.Extension -eq '.rs' -and $relative -notmatch '^(tests|examples|benches)/') {
$text = [IO.File]::ReadAllText($file.FullName)
# Full-line comments, including rustdoc examples, are not compiled
# font embeds. Keep real code and feature-gated macros conservative.
$text = [regex]::Replace($text, '(?m)^[\t ]*//[^\r\n]*', '')
foreach ($match in [regex]::Matches($text, 'include_bytes!\s*\(\s*"([^"]+\.(?:ttf|otf|woff2?))"')) {
$fontPath = [IO.Path]::GetFullPath((Join-Path $file.DirectoryName $match.Groups[1].Value))
$fontRoot = [IO.Path]::GetFullPath($root).TrimEnd('\', '/') + [IO.Path]::DirectorySeparatorChar
$fontComparison = if ([IO.Path]::DirectorySeparatorChar -eq '/') { [StringComparison]::Ordinal } else { [StringComparison]::OrdinalIgnoreCase }
if (-not $fontPath.StartsWith($fontRoot, $fontComparison)) { throw "Embedded font path escapes crate source root: $key/$relative" }
$font = Under-Root $root $fontPath.Substring($fontRoot.Length)
[void]$fontReferences.Add($font.Substring($root.Length + 1).Replace('\', '/'))
}
}
}
if ($isWorkspace) { $notices.Add((Add-Notice (Under-Root $Repository 'LICENSE') "$prefix/workspace-LICENSE")) }
# Registry supplements bind original .crate bytes, not a local patch
# with the same name/version and no registry checksum.
$matching = @($supplements.packages | Where-Object { $registryChecksum -and $_.name -eq $package.name -and $_.version -eq $package.version })
foreach ($supplement in $matching) {
if ($supplement.checksum -ne $registryChecksum) { throw "Supplement is for different registry source: $key" }
foreach ($notice in @(Add-Supplements $supplement.notices "$prefix/supplement")) { $notices.Add($notice) }
}
if (-not $package.license -and -not $package.license_file) { $failures.Add("No license declaration: $key") }
if ($package.license_file) {
$licensePath = Under-Root $root $package.license_file
$notices.Add((Add-Notice $licensePath "$prefix/declared-license/$([IO.Path]::GetFileName($licensePath))"))
$hasLicenseText = $true
}
if (-not $package.source -and -not $isWorkspace -and -not $hasLicenseText) { $failures.Add("Local dependency requires its own license text: $key") }
if ($notices.Count -eq 0) { $failures.Add("No notice text: $key ($($package.license)); provide checksum-bound supplement") }
$assets = New-Object 'Collections.Generic.List[object]'
foreach ($relative in @($fontReferences | Sort-Object)) {
$assetHash = Hash-File (Under-Root $root $relative)
$matches = @($supplements.assets | Where-Object { $_.name -eq $package.name -and $_.version -eq $package.version -and $_.path -eq $relative })
if ($matches.Count -ne 1 -or $matches[0].sha256 -ne $assetHash) { $failures.Add("Embedded font requires exact notice mapping: $key/$relative ($assetHash)"); continue }
$assetNotices = @(Add-Supplements $matches[0].notices "$prefix/assets/$([IO.Path]::GetFileName($relative))")
if ($assetNotices.Count -eq 0) { $failures.Add("No embedded font notice: $key/$relative") }
$assets.Add([ordered]@{ path = $relative; sha256 = $assetHash; license = $matches[0].license; notices = $assetNotices })
}
$records.Add([ordered]@{ id = $id; name = $package.name; version = $package.version; license = $package.license; license_file = $package.license_file; repository = $package.repository; source = $package.source; local_path = $localPath; registry_checksum = $registryChecksum; features = $nodes[$id].features; local_source_sha256 = $sourceHashes; notices = @($notices.ToArray()); embedded_fonts = @($assets.ToArray()) })
}
$rustVersion = @(& rustc -Vv)
if ($LASTEXITCODE -ne 0) { throw 'Unable to identify the selected Rust compiler.' }
$sysroot = @(& rustc --print sysroot)
if ($LASTEXITCODE -ne 0 -or $sysroot.Count -ne 1) { throw 'Unable to locate the selected Rust distribution.' }
$targetLib = @(& rustc --print target-libdir --target $Target)
if ($LASTEXITCODE -ne 0 -or $targetLib.Count -ne 1 -or -not (Test-Path -LiteralPath $targetLib[0] -PathType Container)) { throw "Rust target libraries missing: $Target" }
$rustNotices = @()
foreach ($path in @('share/doc/rust/COPYRIGHT.html', 'share/doc/rust/COPYRIGHT-library.html')) {
$rustNotices += Add-Notice (Under-Root $sysroot[0] $path) "licenses/rust/$([IO.Path]::GetFileName($path))"
}
foreach ($path in @('share/doc/COPYING3', 'share/doc/COPYING.RUNTIME')) {
if (Test-Path -LiteralPath (Join-Path $sysroot[0] $path) -PathType Leaf) { $rustNotices += Add-Notice (Join-Path $sysroot[0] $path) "licenses/rust/$([IO.Path]::GetFileName($path))" }
}
$rustLibraries = [ordered]@{}
foreach ($file in @(Get-ChildItem -LiteralPath $targetLib[0] -File | Sort-Object Name)) { $rustLibraries[$file.Name] = Hash-File $file.FullName }
$runtime = $null
if ($LlvmMinGWRoot) {
$runtimeHash = Hash-File (Under-Root $LlvmMinGWRoot 'x86_64-w64-mingw32/bin/libunwind.dll')
$matches = @($supplements.runtimes | Where-Object { $_.sha256 -eq $runtimeHash -and $_.name -eq 'libunwind.dll' })
if ($matches.Count -ne 1) { $failures.Add("libunwind.dll requires exact component notice mapping: $runtimeHash") }
else {
$runtimeNotices = @(Add-Supplements $matches[0].notices 'licenses/llvm-libunwind')
if (-not $runtimeNotices.Count) { $failures.Add('Missing libunwind component notices') }
$runtime = [ordered]@{ name = 'libunwind.dll'; sha256 = $runtimeHash; provenance = $matches[0].provenance; notices = $runtimeNotices }
}
}
if ($failures.Count -gt 0) { throw ("Notice preflight unresolved:`n - " + ($failures -join "`n - ")) }
return [pscustomobject]@{ Inputs = $inputs; Hashes = $hashes; Inventory = [ordered]@{ format_version = 1; target = $Target; root_packages = @($RootPackages | Sort-Object -Unique); cargo_lock_sha256 = $lockHash; workspace_build_inputs_sha256 = $workspaceInputs; scope = 'Target-filtered locked offline metadata; transitive normal/build edges from the stated root packages. Workspace feature unification may include a conservative superset. No binary/source equivalence or universal license audit claimed.'; packages = @($records.ToArray()); rust = [ordered]@{ version = $rustVersion; notices = $rustNotices; target_libraries_sha256 = $rustLibraries }; runtime = $runtime } }
}
# Compatibility entry point for the reviewed Windows pipeline and its callers.
function Get-WindowsNotices([string]$Repository, [string]$Target, [string]$NoticeManifest, [string]$LlvmMinGWRoot) {
Get-ArcadiaNotices $Repository $Target $NoticeManifest $LlvmMinGWRoot
}