Files
ARCADIA-Studio/scripts/package-windows.ps1

146 lines
7.9 KiB
PowerShell

#Requires -Version 5.1
# Does not build or launch applications.
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[ValidateSet('x86_64-pc-windows-msvc', 'x86_64-pc-windows-gnullvm')]
[string]$Target,
[Parameter(Mandatory = $true)]
[ValidatePattern('^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$')]
[string]$Version,
[Parameter(Mandatory = $true)]
[string]$BinaryDirectory,
[string]$LlvmMinGWRoot,
[string]$BuildEvidence,
[string]$Python = 'python',
[string]$NoticeManifest = (Join-Path $PSScriptRoot 'windows-notices/manifest.json'),
[string]$OutputRoot = (Join-Path $PSScriptRoot '../dist')
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$repository = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..'))
$utf8 = New-Object Text.UTF8Encoding($false)
. (Join-Path $PSScriptRoot 'package-common.ps1')
$windowsInputs = Get-PackageWindowsInputs $BinaryDirectory $Target $LlvmMinGWRoot @('arcadia-studio.exe', 'arcadia-server.exe')
$inputs = $windowsInputs.Inputs
$imports = $windowsInputs.Imports
$inspectedHashes = $windowsInputs.InspectedHashes
$binaryHashes = $windowsInputs.BinaryHashes
$evidence = Read-PackageBuildEvidence $BuildEvidence $Target $binaryHashes
$documentation = Get-PackageDocumentation $repository 'windows' $Python
foreach ($name in $documentation.Keys) { $inputs[$name] = $documentation[$name] }
# Resolve notice/source inputs before creating an output claim. No Cargo build occurs.
. (Join-Path $PSScriptRoot 'windows-notices.ps1')
$notices = Get-WindowsNotices $repository $Target $NoticeManifest $LlvmMinGWRoot
foreach ($name in $notices.Inputs.Keys) {
if ($inputs.Contains($name)) { throw "Notice collides with package input: $name" }
$inputs[$name] = $notices.Inputs[$name]
$inspectedHashes[$name] = $notices.Hashes[$name]
}
$output = [IO.Path]::GetFullPath($OutputRoot)
$packageName = "ArcadiaStudio-$Version-$Target"
$stage = Join-Path $output $packageName
$archive = "$stage.zip"
$checksum = "$archive.sha256"
$lockPath = "$stage.packaging.lock"
foreach ($path in @($stage, $archive, $checksum, $lockPath)) {
if (Test-Path -LiteralPath $path) { throw "Output already exists; choose a fresh version or output root: $path" }
}
foreach ($directory in @('docs', 'examples', 'deployment')) {
$sourceRoot = [IO.Path]::GetFullPath((Join-Path $repository $directory)) + [IO.Path]::DirectorySeparatorChar
if (($stage + [IO.Path]::DirectorySeparatorChar).StartsWith($sourceRoot, [StringComparison]::OrdinalIgnoreCase)) {
throw 'Output must not be inside a packaged source directory.'
}
}
[void][IO.Directory]::CreateDirectory($output)
$lock = [IO.File]::Open($lockPath, [IO.FileMode]::CreateNew, [IO.FileAccess]::Write, [IO.FileShare]::None)
try {
[void][IO.Directory]::CreateDirectory($stage)
$inputHashes = [ordered]@{}
foreach ($name in $inputs.Keys) {
Assert-PackageOrdinaryFile $inputs[$name]
$destination = Join-Path $stage $name
[void][IO.Directory]::CreateDirectory([IO.Path]::GetDirectoryName($destination))
$before = (Get-FileHash -LiteralPath $inputs[$name] -Algorithm SHA256).Hash.ToLowerInvariant()
if ($inspectedHashes.ContainsKey($name) -and $inspectedHashes[$name] -ne $before) {
throw "Input changed after dependency/notice inspection: $name"
}
[IO.File]::Copy($inputs[$name], $destination, $false)
if ((Get-FileHash -LiteralPath $destination -Algorithm SHA256).Hash.ToLowerInvariant() -ne $before) {
throw "Input changed during staging: $name"
}
$inputHashes[$name] = $before
}
$metadata = [ordered]@{
version = $Version; target = $Target
acceptance = 'Packaging integrity only; native clean-environment verification required.'
build = 'Prebuilt inputs supplied by caller; this script does not establish build profile or source revision.'
build_evidence = $evidence; imports = $imports; input_sha256 = $inputHashes
}
[IO.File]::WriteAllText((Join-Path $stage 'package.json'), ($metadata | ConvertTo-Json -Depth 14) + "`n", $utf8)
Write-PackageNotices $stage $notices
$start = @'
Arcadia Studio portable Windows bundle
Open arcadia-studio.exe. See docs/windows-packaging.md for runtime prerequisites
and verification, README.md for model workflows, and docs/server.md to run the
optional arcadia-server.exe. Examples are under examples/; Help is built in.
Keep the bundled runtime DLL beside both executables when present. No compiler
installation or machine PATH change is required by the staged runtime closure.
This binary bundle does not include repository sources needed for Cargo/Docker
build commands. Packaging integrity is not native release acceptance; read
package.json and the distributor's binary-specific acceptance record for limitations.
'@
[IO.File]::WriteAllText((Join-Path $stage 'START-HERE.txt'), $start + "`n", $utf8)
$documentCheck = Invoke-PackageHelper $Python @('check-docs', '--stage', $stage)
[IO.File]::WriteAllText((Join-Path $stage 'documentation-check.json'), ($documentCheck | ConvertTo-Json) + "`n", $utf8)
$manifest = 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('\', '/')
$manifest.Add((Get-FileHash -LiteralPath $file.FullName -Algorithm SHA256).Hash.ToLowerInvariant() + ' ' + $relative)
}
[IO.File]::WriteAllText((Join-Path $stage 'SHA256SUMS.txt'), ($manifest -join "`n") + "`n", $utf8)
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() }
$archiveHash = (Get-FileHash -LiteralPath $archive -Algorithm SHA256).Hash.ToLowerInvariant()
[IO.File]::WriteAllText($checksum, "$archiveHash $packageName.zip`n", $utf8)
[pscustomobject]@{ Directory = $stage; Archive = $archive; SHA256 = $archiveHash; Files = $seen.Count; NativeAcceptance = 'Pending' }
} finally {
$lock.Dispose()
# Keep the claim file and any partial outputs: retries never overwrite an earlier attempt.
}