#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 } }