Files
ARCADIA-Studio/scripts/package-common.ps1
T

221 lines
13 KiB
PowerShell

#Requires -Version 5.1
# Shared prebuilt-artifact packaging helpers. No builds or downloads.
function Assert-PackageOrdinaryFile([string]$Path) {
$item = Get-Item -LiteralPath $Path
if ($item.PSIsContainer) { throw "Package input is a directory: $Path" }
$current = $item
while ($null -ne $current) {
if (($current.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) { throw "Package input has a symlink/reparse ancestor: $($current.FullName)" }
$current = if ($current -is [IO.FileInfo]) { $current.Directory } else { $current.Parent }
}
}
function Invoke-PackageHelper([string]$Python, [string[]]$Arguments) {
$raw = @(& $Python (Join-Path $PSScriptRoot 'package-artifacts.py') @Arguments)
if ($LASTEXITCODE -ne 0) { throw 'Package artifact preflight/verification failed.' }
if ($raw.Count) { return (($raw -join "`n") | ConvertFrom-Json) }
}
function Get-PackageDocumentation([string]$Repository, [string]$Platform, [string]$Python) {
$result = Invoke-PackageHelper $Python @('docs', '--repository', $Repository, '--platform', $Platform)
$inputs = [ordered]@{}
foreach ($entry in $result.files) { $inputs[$entry.path] = $entry.source }
return $inputs
}
function Read-PackageBuildEvidence([string]$Path, [string]$Target, $BinaryHashes) {
if (-not $Path) { return $null }
$evidence = Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json
foreach ($field in @('source_identity', 'profile', 'compiler', 'target', 'binary_sha256')) {
if (-not $evidence.PSObject.Properties[$field]) { throw "Build evidence missing $field" }
}
if ($evidence.target -ne $Target) { throw 'Build evidence target differs from requested package target.' }
foreach ($field in @('source_identity', 'profile', 'compiler')) {
if ($evidence.$field -isnot [string] -or [string]::IsNullOrWhiteSpace($evidence.$field)) { throw "Build evidence $field must be explicit nonempty text." }
}
foreach ($name in $BinaryHashes.Keys) {
if (-not $evidence.binary_sha256.PSObject.Properties[$name] -or $evidence.binary_sha256.$name -cne $BinaryHashes[$name]) { throw "Build evidence binary hash mismatch: $name" }
}
return [ordered]@{ assertion = 'Caller-supplied build evidence; target and binary hashes checked, source/compiler/profile statements not inferred or independently proven by packaging.'; evidence = $evidence }
}
function Write-PackageNotices([string]$Stage, $Notices) {
$encoding = New-Object Text.UTF8Encoding($false)
[IO.File]::WriteAllText((Join-Path $Stage 'dependency-notices.json'), ($Notices.Inventory | ConvertTo-Json -Depth 18) + "`n", $encoding)
$index = @('Third-party notice inventory', '', 'Exact versions, license expressions, source hashes and notice paths: dependency-notices.json', 'License texts and embedded-asset notices: licenses/', 'Rust distribution notices: licenses/rust/', 'Collection does not establish binary/source equivalence or a universal license audit.', '')
foreach ($package in $Notices.Inventory.packages) { $index += "$($package.name) $($package.version) -- $($package.license) -- licenses/dependencies/$($package.name)-$($package.version)/" }
[IO.File]::WriteAllText((Join-Path $Stage 'THIRD-PARTY-NOTICES.txt'), ($index -join "`n") + "`n", $encoding)
}
function Copy-PackageInputs([string]$Stage, $Inputs, $InspectedHashes) {
$hashes = [ordered]@{}
foreach ($name in $Inputs.Keys) {
Assert-PackageOrdinaryFile $Inputs[$name]
$source = Get-Item -LiteralPath $Inputs[$name]
if ($source.PSIsContainer -or ($source.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) { throw "Package input must be an ordinary file: $name" }
$destination = Join-Path $Stage $name
[void][IO.Directory]::CreateDirectory([IO.Path]::GetDirectoryName($destination))
$before = (Get-FileHash -LiteralPath $source.FullName -Algorithm SHA256).Hash.ToLowerInvariant()
if ($InspectedHashes.ContainsKey($name) -and $InspectedHashes[$name] -ne $before) { throw "Input changed after inspection: $name" }
[IO.File]::Copy($source.FullName, $destination, $false)
if ((Get-FileHash -LiteralPath $destination -Algorithm SHA256).Hash.ToLowerInvariant() -ne $before) { throw "Input changed during staging: $name" }
$hashes[$name] = $before
}
return $hashes
}
function Write-PackageChecksums([string]$Stage) {
$encoding = New-Object Text.UTF8Encoding($false)
$lines = New-Object 'Collections.Generic.List[string]'
foreach ($file in Get-ChildItem -LiteralPath $Stage -Recurse -File | Sort-Object FullName) {
$relative = $file.FullName.Substring($Stage.Length + 1).Replace('\', '/')
if ($relative -eq 'SHA256SUMS.txt') { throw 'Package checksum manifest already exists.' }
$lines.Add((Get-FileHash -LiteralPath $file.FullName -Algorithm SHA256).Hash.ToLowerInvariant() + ' ' + $relative)
}
[IO.File]::WriteAllText((Join-Path $Stage 'SHA256SUMS.txt'), ($lines -join "`n") + "`n", $encoding)
}
function Require-PackageFile([string]$Path) {
if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { throw "Required file missing: $Path" }
Assert-PackageOrdinaryFile $Path
return (Get-Item -LiteralPath $Path).FullName
}
# Read ordinary PE32+ imports without invoking a compiler or trusting PATH tools.
# Dynamically loaded libraries and delay imports remain a separate runtime smoke gate.
function Read-PackagePeImports([string]$Path) {
$stream = [IO.File]::OpenRead($Path)
$reader = New-Object IO.BinaryReader($stream)
try {
if ($reader.ReadUInt16() -ne 0x5a4d) { throw "Not a PE executable: $Path" }
$stream.Position = 0x3c
$pe = $reader.ReadUInt32()
$stream.Position = $pe
if ($reader.ReadUInt32() -ne 0x4550) { throw "Invalid PE signature: $Path" }
if ($reader.ReadUInt16() -ne 0x8664) { throw "Expected x86_64 PE input: $Path" }
$sectionCount = $reader.ReadUInt16()
$stream.Position = $pe + 20
$optionalSize = $reader.ReadUInt16()
$optional = $pe + 24
$stream.Position = $optional
if ($reader.ReadUInt16() -ne 0x20b) { throw "Expected PE32+ input: $Path" }
$stream.Position = $optional + 120
$importRva = $reader.ReadUInt32()
$sections = @()
for ($index = 0; $index -lt $sectionCount; $index++) {
$stream.Position = $optional + $optionalSize + $index * 40 + 8
$virtualSize = $reader.ReadUInt32()
$virtualAddress = $reader.ReadUInt32()
$rawSize = $reader.ReadUInt32()
$rawAddress = $reader.ReadUInt32()
$sections += [pscustomobject]@{ Start = $virtualAddress; Size = [Math]::Max($virtualSize, $rawSize); Raw = $rawAddress }
}
function Rva-Offset([uint32]$Rva) {
foreach ($section in $sections) {
if ($Rva -ge $section.Start -and $Rva -lt ($section.Start + $section.Size)) {
return [long]($section.Raw + $Rva - $section.Start)
}
}
throw "Unmapped PE RVA in $Path"
}
if ($importRva -eq 0) { return }
$descriptors = Rva-Offset $importRva
for ($index = 0; $index -lt 4096; $index++) {
$stream.Position = $descriptors + $index * 20
$fields = @(0..4 | ForEach-Object { $reader.ReadUInt32() })
if (@($fields | Where-Object { $_ -ne 0 }).Count -eq 0) { return }
$stream.Position = Rva-Offset $fields[3]
$name = New-Object Text.StringBuilder
for ($character = 0; $character -lt 256; $character++) {
$value = $reader.ReadByte()
if ($value -eq 0) { break }
[void]$name.Append([char]$value)
}
if ($character -eq 256) { throw "Unterminated PE import name: $Path" }
$name.ToString().ToLowerInvariant()
}
throw "Unterminated PE import table: $Path"
} finally {
$reader.Dispose()
}
}
function Get-PackageWindowsInputs([string]$BinaryDirectory, [string]$Target, [string]$LlvmMinGWRoot, [string[]]$BinaryNames) {
if ($Target -notin @('x86_64-pc-windows-msvc', 'x86_64-pc-windows-gnullvm')) { throw 'Unsupported Windows target.' }
if ($BinaryNames.Count -eq 0 -or @($BinaryNames | Where-Object { $_ -notin @('arcadia.exe', 'arcadia-studio.exe', 'arcadia-server.exe') }).Count -gt 0) { throw 'Unsupported Windows package binary.' }
# Windows system components only. Unknown imports fail, including VC redistributable DLLs.
# Add a redistributable only with explicit provenance, license, and recursive import checks.
$systemDlls = @(
'kernel32.dll', 'kernelbase.dll', 'user32.dll', 'ole32.dll', 'oleaut32.dll',
'bcrypt.dll', 'bcryptprimitives.dll', 'advapi32.dll', 'dwmapi.dll', 'gdi32.dll',
'shell32.dll', 'imm32.dll', 'uxtheme.dll', 'ws2_32.dll', 'ntdll.dll', 'opengl32.dll',
'ucrtbase.dll', 'msvcrt.dll', 'crypt32.dll', 'secur32.dll', 'shlwapi.dll',
'comdlg32.dll', 'comctl32.dll', 'version.dll', 'winmm.dll', 'winhttp.dll',
'iphlpapi.dll', 'userenv.dll', 'dbghelp.dll', 'dwrite.dll', 'd2d1.dll',
'd3d11.dll', 'd3d12.dll', 'dxgi.dll', 'd3dcompiler_47.dll', 'propsys.dll',
'powrprof.dll', 'setupapi.dll', 'cfgmgr32.dll', 'winspool.drv', 'wtsapi32.dll'
)
$inputs = [ordered]@{}
foreach ($name in $BinaryNames) {
$inputs[$name] = Require-PackageFile (Join-Path $BinaryDirectory $name)
}
if ($Target -eq 'x86_64-pc-windows-gnullvm') {
if (-not $LlvmMinGWRoot) { throw 'gnullvm requires -LlvmMinGWRoot for its exact runtime and license.' }
$inputs['libunwind.dll'] = Require-PackageFile (Join-Path $LlvmMinGWRoot 'x86_64-w64-mingw32/bin/libunwind.dll')
$inputs['licenses/llvm-mingw-LICENSE.TXT'] = Require-PackageFile (Join-Path $LlvmMinGWRoot 'LICENSE.TXT')
} elseif ($LlvmMinGWRoot) {
throw 'MSVC packaging does not use LLVM-MinGW; omit -LlvmMinGWRoot.'
}
$imports = [ordered]@{}
$inspectedHashes = @{}
foreach ($name in @($inputs.Keys | Where-Object { $_ -match '\.(exe|dll)$' })) {
$inspectedHashes[$name] = (Get-FileHash -LiteralPath $inputs[$name] -Algorithm SHA256).Hash.ToLowerInvariant()
$imports[$name] = @(Read-PackagePeImports $inputs[$name] | Sort-Object -Unique)
foreach ($dependency in $imports[$name]) {
$isSystem = $dependency -match '^(api-ms-win-|ext-ms-win-)[a-z0-9-]+\.dll$' -or $systemDlls -contains $dependency
if (-not $isSystem -and -not $inputs.Contains($dependency)) {
throw "Unpackaged runtime import '$dependency' in '$name'. Resolve its provenance and dependency closure before packaging."
}
}
}
$binaryHashes = [ordered]@{}
foreach ($name in $BinaryNames) { $binaryHashes[$name] = $inspectedHashes[$name] }
return [pscustomobject]@{ Inputs = $inputs; Imports = $imports; InspectedHashes = $inspectedHashes; BinaryHashes = $binaryHashes }
}
function Write-PackageWindowsArchive([string]$Stage, [string]$Archive, [string]$PackageName) {
Add-Type -AssemblyName System.IO.Compression
Add-Type -AssemblyName System.IO.Compression.FileSystem
$zipStream = [IO.File]::Open($archive, [IO.FileMode]::CreateNew)
$zip = New-Object IO.Compression.ZipArchive($zipStream, [IO.Compression.ZipArchiveMode]::Create)
try {
foreach ($file in Get-ChildItem -LiteralPath $stage -Recurse -File | Sort-Object FullName) {
$relative = $file.FullName.Substring($stage.Length + 1).Replace('\', '/')
$entry = $zip.CreateEntry("$packageName/$relative", [IO.Compression.CompressionLevel]::Optimal)
$entry.LastWriteTime = [DateTimeOffset]::new(2000, 1, 1, 0, 0, 0, [TimeSpan]::Zero)
$destination = $entry.Open()
$source = $file.OpenRead()
try { $source.CopyTo($destination) } finally { $source.Dispose(); $destination.Dispose() }
}
} finally { $zip.Dispose(); $zipStream.Dispose() }
$zip = [IO.Compression.ZipFile]::OpenRead($archive)
try {
$seen = New-Object 'Collections.Generic.HashSet[string]' ([StringComparer]::OrdinalIgnoreCase)
foreach ($entry in $zip.Entries) {
if (-not $seen.Add($entry.FullName)) { throw "Duplicate archive entry: $($entry.FullName)" }
$relative = $entry.FullName.Substring($packageName.Length + 1)
$source = $entry.Open()
$sha = [Security.Cryptography.SHA256]::Create()
try { $actual = [BitConverter]::ToString($sha.ComputeHash($source)).Replace('-', '').ToLowerInvariant() }
finally { $sha.Dispose(); $source.Dispose() }
$expected = (Get-FileHash -LiteralPath (Join-Path $stage $relative) -Algorithm SHA256).Hash.ToLowerInvariant()
if ($actual -ne $expected) { throw "Archive mismatch: $relative" }
}
if ($seen.Count -ne @(Get-ChildItem -LiteralPath $stage -Recurse -File).Count) { throw 'Archive file count mismatch.' }
} finally { $zip.Dispose() }
return [pscustomobject]@{ SHA256 = (Get-FileHash -LiteralPath $Archive -Algorithm SHA256).Hash.ToLowerInvariant(); Files = $seen.Count }
}