Package standalone source CLI with verified runtime and notices
This commit is contained in:
@@ -74,3 +74,147 @@ function Write-PackageChecksums([string]$Stage) {
|
||||
}
|
||||
[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 }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user