Package standalone source CLI with verified runtime and notices

This commit is contained in:
2026-09-11 05:48:05 -07:00
parent 6a610616e7
commit 27df4a3945
23 changed files with 2239 additions and 115 deletions
+21 -9
View File
@@ -81,22 +81,33 @@ def linked_path(origin, link):
return '/'.join(parts)
def document_inputs(repository, platform):
def document_inputs(repository, platform, product='studio'):
repository = Path(repository).resolve()
raw = subprocess.check_output(['git', '-C', str(repository), 'ls-files', '-z'])
tracked = set(raw.decode('utf-8').split('\0')) - {''}
roots = ['LICENSE', 'docs/server.md', 'docs/api.rst', 'docs/api.html',
'docs/language.rst', 'docs/language.html', f'docs/{platform}-packaging.md',
'deployment/Dockerfile', 'deployment/Dockerfile.dockerignore',
'deployment/arcadia-server.service']
roots += sorted(name for name in tracked if name.startswith('examples/'))
if product == 'studio':
roots = ['LICENSE', 'docs/server.md', 'docs/api.rst', 'docs/api.html',
'docs/language.rst', 'docs/language.html', f'docs/{platform}-packaging.md',
'deployment/Dockerfile', 'deployment/Dockerfile.dockerignore',
'deployment/arcadia-server.service']
roots += sorted(name for name in tracked if name.startswith('examples/'))
readme = 'scripts/package-readme.md'
elif product == 'cli':
roots = ['LICENSE', 'docs/source-cli.md', 'docs/language.rst', 'docs/language.html',
'examples/text-first/README.md']
# The text-first guide links the modular water-pressure walkthrough;
# ship its whole model/evidence directory, not just its Markdown links.
roots += sorted(name for name in tracked if name.startswith(('examples/text-first/', 'examples/release1/')))
readme = 'scripts/package-cli-readme.md'
else:
raise ValueError(f'Unknown package product: {product}')
# Two explicitly authored inputs can be packaged before their first commit.
permitted = tracked | set(roots) | {'scripts/package-readme.md', 'docs/linux-packaging.md', 'docs/windows-packaging.md'}
permitted = tracked | set(roots) | {readme, 'docs/linux-packaging.md', 'docs/windows-packaging.md'}
def repository_file(name):
candidate = ordinary(repository / name)
candidate.resolve().relative_to(repository)
return candidate
sources = {'README.md': repository_file('scripts/package-readme.md')}
sources = {'README.md': repository_file(readme)}
for name in roots:
sources[name] = repository_file(name)
queue = list(sources)
@@ -335,6 +346,7 @@ def main():
docs = commands.add_parser('docs')
docs.add_argument('--repository', required=True)
docs.add_argument('--platform', choices=['windows', 'linux'], required=True)
docs.add_argument('--product', choices=['studio', 'cli'], default='studio')
check = commands.add_parser('check-docs')
check.add_argument('--stage', required=True)
elf = commands.add_parser('elf')
@@ -349,7 +361,7 @@ def main():
notices.add_argument('--target', required=True)
args = parser.parse_args()
if args.command == 'docs':
result = document_inputs(args.repository, args.platform)
result = document_inputs(args.repository, args.platform, args.product)
elif args.command == 'check-docs':
result = check_documents(args.stage)
elif args.command == 'elf':
+38
View File
@@ -0,0 +1,38 @@
# Arcadia CLI portable Windows bundle
Extract the whole archive into a writable directory. Open a terminal there:
```powershell
.\arcadia.exe --version
.\arcadia.exe --help
.\arcadia.exe check examples/text-first/project.arcadia
```
Keep `libunwind.dll` beside `arcadia.exe` when included. The x86-64 gnullvm bundle
uses the Windows Universal C Runtime. No compiler installation, desktop editor,
network service or machine PATH change is needed to use this executable.
Read the [CLI command and diagnostic contract](docs/source-cli.md), the
[text-first examples](examples/text-first/README.md) and the
[language reference](docs/language.html) ([text source](docs/language.rst)).
Copy a complete example folder into your own work directory before editing it.
Supply the project entry file explicitly. Editing commands preview by default;
`--write` saves the candidate prepared by that invocation. A later invocation
may choose different provisional UUIDs for records whose IDs are still missing.
Some example walkthroughs describe Studio controls such as Apply and Save.
For this CLI, edit the copied text in your editor, run `arcadia check` on its
entry, review `arcadia stabilize project.arcadia`, then use
`arcadia stabilize project.arcadia --write` to save
missing identities. The full `examples/release1` model and evidence are included
for studying modular source; running its visual walkthrough requires Studio.
The build instructions in the CLI documentation refer to a separate source
checkout; this archive contains the executable and examples. It does not include
repository sources needed for Cargo commands. The published milestone's source
and exact build evidence are identified in its release record and `package.json`.
`SHA256SUMS.txt` records the package files. `THIRD-PARTY-NOTICES.txt`,
`dependency-notices.json` and `licenses/` retain exact dependency, embedded-asset,
Rust and runtime notices. Packaging integrity does not establish runtime
acceptance; read the distributor's binary-specific acceptance record.
+75
View File
@@ -0,0 +1,75 @@
#Requires -Version 5.1
# Package an already-built CLI. No builds, downloads, recovery or application launch.
[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,
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]$BuildEvidence,
[string]$LlvmMinGWRoot,
[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 '..'))
$encoding = New-Object Text.UTF8Encoding($false)
. (Join-Path $PSScriptRoot 'package-common.ps1')
. (Join-Path $PSScriptRoot 'windows-notices.ps1')
$windowsInputs = Get-PackageWindowsInputs $BinaryDirectory $Target $LlvmMinGWRoot @('arcadia.exe')
$inputs = $windowsInputs.Inputs
$inspectedHashes = $windowsInputs.InspectedHashes
$evidence = Read-PackageBuildEvidence $BuildEvidence $Target $windowsInputs.BinaryHashes
$documentation = Invoke-PackageHelper $Python @('docs', '--repository', $repository, '--platform', 'windows', '--product', 'cli')
foreach ($entry in $documentation.files) { $inputs[$entry.path] = $entry.source }
$notices = Get-ArcadiaNotices $repository $Target $NoticeManifest $LlvmMinGWRoot @('oa-cli')
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 = "ArcadiaCLI-$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', 'crates', 'scripts')) {
$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 = Copy-PackageInputs $stage $inputs $inspectedHashes
$metadata = [ordered]@{
product = 'Arcadia CLI'; version = $Version; target = $Target
acceptance = 'Packaging integrity only; clean-environment CLI verification required.'
build = 'Prebuilt input; source/compiler/profile assertions are recorded, not inferred.'
build_evidence = $evidence; imports = $windowsInputs.Imports; input_sha256 = $inputHashes
}
[IO.File]::WriteAllText((Join-Path $stage 'package.json'), ($metadata | ConvertTo-Json -Depth 14) + "`n", $encoding)
Write-PackageNotices $stage $notices
[IO.File]::WriteAllText((Join-Path $stage 'START-HERE.txt'), "Arcadia CLI portable Windows bundle`nOpen a terminal in this folder and run .\arcadia.exe --help.`nRead README.md and docs/source-cli.md. Keep any bundled runtime DLL beside arcadia.exe.`nNo compiler, desktop editor, network service or machine PATH change is required.`nCopy an example into your own work directory before editing it. Preview is the default; --write saves.`nPackaging integrity does not establish runtime acceptance; see package.json and release evidence.`n", $encoding)
$documentCheck = Invoke-PackageHelper $Python @('check-docs', '--stage', $stage)
[IO.File]::WriteAllText((Join-Path $stage 'documentation-check.json'), ($documentCheck | ConvertTo-Json) + "`n", $encoding)
Write-PackageChecksums $stage
$result = Write-PackageWindowsArchive $stage $archive $packageName
[IO.File]::WriteAllText($checksum, "$($result.SHA256) $packageName.zip`n", $encoding)
[pscustomobject]@{ Directory = $stage; Archive = $archive; SHA256 = $result.SHA256; Files = $result.Files; NativeAcceptance = 'Pending' }
} finally {
$lock.Dispose()
# Keep claims and partial outputs. A retry must never overwrite an earlier attempt.
}
+144
View File
@@ -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 }
}
+5 -103
View File
@@ -23,109 +23,11 @@ $repository = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..'))
$utf8 = New-Object Text.UTF8Encoding($false)
. (Join-Path $PSScriptRoot 'package-common.ps1')
function Require-File([string]$Path) {
if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { throw "Required file missing: $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-PeImports([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()
}
}
# 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 @('arcadia-studio.exe', 'arcadia-server.exe')) {
$inputs[$name] = Require-File (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-File (Join-Path $LlvmMinGWRoot 'x86_64-w64-mingw32/bin/libunwind.dll')
$inputs['licenses/llvm-mingw-LICENSE.TXT'] = Require-File (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-PeImports $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 @('arcadia-studio.exe', 'arcadia-server.exe')) { $binaryHashes[$name] = $inspectedHashes[$name] }
$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] }
+38
View File
@@ -61,6 +61,44 @@ class PackageContracts(unittest.TestCase):
self.assertEqual(archive.getmember('fixture/examples/temperature delta.txt').mode, 0o644)
self.assertEqual(len(archive.getmembers()), 3)
def test_cli_document_closure_uses_own_readme_and_transitive_links(self):
names = ['LICENSE', 'docs/source-cli.md', 'docs/language.rst', 'docs/language.html',
'examples/text-first/README.md', 'examples/text-first/project.arcadia',
'docs/linked.md', 'docs/linked.html', 'examples/release1/oa/module.arcadia']
for name in names:
self.file(name, b'fixture')
self.file('scripts/package-cli-readme.md', b'[CLI](docs/source-cli.md)')
self.file('docs/source-cli.md', b'[More](linked.md)')
self.file('docs/linked.md', b'[HTML](linked.html)')
with patch.object(packaging.subprocess, 'check_output', return_value='\0'.join(names).encode()):
result = packaging.document_inputs(self.root, 'windows', 'cli')
mapped = {entry['path']: entry['source'] for entry in result['files']}
self.assertEqual(Path(mapped['README.md']).name, 'package-cli-readme.md')
self.assertIn('docs/linked.html', mapped)
self.assertIn('examples/text-first/project.arcadia', mapped)
self.assertIn('examples/release1/oa/module.arcadia', mapped)
self.assertNotIn('docs/server.md', mapped)
self.file('docs/linked.md', b'[Missing](untracked.md)')
with patch.object(packaging.subprocess, 'check_output', return_value='\0'.join(names).encode()):
with self.assertRaisesRegex(ValueError, 'Untracked/missing'):
packaging.document_inputs(self.root, 'windows', 'cli')
def test_default_document_roots_equal_explicit_studio(self):
names = ['LICENSE', 'docs/server.md', 'docs/api.rst', 'docs/api.html',
'docs/language.rst', 'docs/language.html', 'docs/windows-packaging.md',
'deployment/Dockerfile', 'deployment/Dockerfile.dockerignore',
'deployment/arcadia-server.service', 'examples/sample.arcadia']
for name in names:
self.file(name, b'fixture')
self.file('scripts/package-readme.md', b'Studio fixture')
with patch.object(packaging.subprocess, 'check_output', return_value='\0'.join(names).encode()):
default = packaging.document_inputs(self.root, 'windows')
explicit = packaging.document_inputs(self.root, 'windows', 'studio')
with self.assertRaisesRegex(ValueError, 'Unknown package product'):
packaging.document_inputs(self.root, 'windows', 'unknown')
self.assertEqual(default, explicit)
self.assertEqual({entry['path'] for entry in default['files']}, set(names) | {'README.md'})
def test_archive_refuses_overwrite_and_case_collisions(self):
self.file('stage/a', b'first')
archive = self.file('archive.tar.gz', b'preserve')
+73
View File
@@ -0,0 +1,73 @@
#Requires -Version 5.1
# Inert PE fixtures and archive checks. No Cargo, builds or executable launch.
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest
. (Join-Path $PSScriptRoot 'package-common.ps1')
$fixture = Join-Path ([IO.Path]::GetTempPath()) ('arcadia-cli-package-' + [guid]::NewGuid().ToString('N'))
[void][IO.Directory]::CreateDirectory($fixture)
$encoding = New-Object Text.UTF8Encoding($false)
$checks = 0
function Assert([bool]$Condition, [string]$Message) { if (-not $Condition) { throw $Message }; $script:checks++ }
function Refuses([scriptblock]$Action, [string]$Pattern) {
try { & $Action; throw 'Expected refusal' } catch { Assert ($_.ToString() -match $Pattern) "Wrong refusal: $_" }
}
function Write-Pe([string]$Path, [string]$Import = 'kernel32.dll', [uint16]$Machine = 0x8664) {
[void][IO.Directory]::CreateDirectory([IO.Path]::GetDirectoryName($Path))
$data = New-Object byte[] 1024
$stream = [IO.MemoryStream]::new($data, $true)
$writer = [IO.BinaryWriter]::new($stream)
try {
$writer.Write([uint16]0x5a4d)
$stream.Position = 0x3c; $writer.Write([uint32]0x80)
$stream.Position = 0x80; $writer.Write([uint32]0x4550); $writer.Write($Machine); $writer.Write([uint16]1)
$stream.Position = 0x94; $writer.Write([uint16]0xf0)
$stream.Position = 0x98; $writer.Write([uint16]0x20b)
$stream.Position = 0x110; $writer.Write([uint32]0x1000)
$stream.Position = 0x190; $writer.Write([uint32]512); $writer.Write([uint32]0x1000); $writer.Write([uint32]512); $writer.Write([uint32]512)
$stream.Position = 524; $writer.Write([uint32]0x1040)
$stream.Position = 576; $writer.Write([Text.Encoding]::ASCII.GetBytes($Import)); $writer.Write([byte]0)
[IO.File]::WriteAllBytes($Path, $data)
} finally { $writer.Dispose() }
}
$bin = Join-Path $fixture 'bin'
$cli = Join-Path $bin 'arcadia.exe'
Write-Pe $cli
$inspected = Get-PackageWindowsInputs $bin x86_64-pc-windows-msvc '' @('arcadia.exe')
Assert (($inspected.Imports['arcadia.exe'] -join ',') -eq 'kernel32.dll') 'System import parse failed'
Assert ($inspected.Inputs.Count -eq 1) 'CLI inspection requires unrelated binaries'
Refuses { Get-PackageWindowsInputs $bin x86_64-pc-windows-msvc '' @('arcadia-studio.exe', 'arcadia-server.exe') } 'Required file missing'
Refuses { Get-PackageWindowsInputs $bin x86_64-pc-windows-gnullvm '' @('arcadia.exe') } 'requires -LlvmMinGWRoot'
Refuses { Get-PackageWindowsInputs $bin unsupported '' @('arcadia.exe') } 'Unsupported Windows target'
Write-Pe $cli 'unknown-runtime.dll'
Refuses { Get-PackageWindowsInputs $bin x86_64-pc-windows-msvc '' @('arcadia.exe') } 'Unpackaged runtime import'
Write-Pe $cli 'kernel32.dll' 0x14c
Refuses { Get-PackageWindowsInputs $bin x86_64-pc-windows-msvc '' @('arcadia.exe') } 'Expected x86_64'
Write-Pe $cli 'libunwind.dll'
$runtime = Join-Path $fixture 'runtime'
$dll = Join-Path $runtime 'x86_64-w64-mingw32/bin/libunwind.dll'
Write-Pe $dll 'unknown-transitive.dll'
[IO.File]::WriteAllText((Join-Path $runtime 'LICENSE.TXT'), 'Synthetic license', $encoding)
Refuses { Get-PackageWindowsInputs $bin x86_64-pc-windows-gnullvm $runtime @('arcadia.exe') } 'Unpackaged runtime import'
Write-Pe $dll
$inspected = Get-PackageWindowsInputs $bin x86_64-pc-windows-gnullvm $runtime @('arcadia.exe')
Assert ($inspected.Inputs.Count -eq 3) 'Runtime and license closure absent'
$evidencePath = Join-Path $fixture 'evidence.json'
$evidence = @{source_identity='test'; profile='inert'; compiler='none'; target='x86_64-pc-windows-gnullvm'; binary_sha256=@{'arcadia.exe'='wrong'}}
[IO.File]::WriteAllText($evidencePath, ($evidence | ConvertTo-Json), $encoding)
Refuses { Read-PackageBuildEvidence $evidencePath x86_64-pc-windows-gnullvm $inspected.BinaryHashes } 'binary hash mismatch'
$evidence.binary_sha256['arcadia.exe'] = $inspected.BinaryHashes['arcadia.exe']
[IO.File]::WriteAllText($evidencePath, ($evidence | ConvertTo-Json), $encoding)
$null = Read-PackageBuildEvidence $evidencePath x86_64-pc-windows-gnullvm $inspected.BinaryHashes
Refuses { Read-PackageBuildEvidence $evidencePath x86_64-pc-windows-msvc $inspected.BinaryHashes } 'target differs'
$stage = Join-Path $fixture 'stage'
[void][IO.Directory]::CreateDirectory($stage)
$null = Copy-PackageInputs $stage $inspected.Inputs $inspected.InspectedHashes
Write-PackageChecksums $stage
$first = Write-PackageWindowsArchive $stage (Join-Path $fixture 'first.zip') 'same-name'
$second = Write-PackageWindowsArchive $stage (Join-Path $fixture 'second.zip') 'same-name'
Assert ($first.SHA256 -eq $second.SHA256 -and $first.Files -eq 4) 'ZIP is not deterministic/exact'
Refuses { Write-PackageWindowsArchive $stage (Join-Path $fixture 'first.zip') 'same-name' } 'already exists'
Assert ((Get-FileHash (Join-Path $fixture 'first.zip')).Hash.ToLowerInvariant() -eq $first.SHA256) 'Refusal changed archive'
Write-Pe $cli 'kernelbase.dll'
Refuses { Copy-PackageInputs (Join-Path $fixture 'changed-stage') $inspected.Inputs $inspected.InspectedHashes } 'Input changed after inspection'
Write-Output "PASS: $checks CLI/Windows packaging assertions; inert fixture retained at $fixture"
+11 -2
View File
@@ -20,7 +20,7 @@ $null = Write-Fixture 'rust/share/doc/rust/COPYRIGHT.html' 'Synthetic Rust distr
$null = Write-Fixture 'rust/share/doc/rust/COPYRIGHT-library.html' 'Synthetic Rust library notice'
$null = Write-Fixture 'rust/lib/fake.rlib' 'Synthetic target library'
$packages = @(); $nodes = @(); $lock = @()
foreach ($name in @('oa-desktop', 'oa-server', 'normal', 'build', 'dev-only', 'unreachable')) {
foreach ($name in @('oa-desktop', 'oa-server', 'oa-cli', 'normal', 'build', 'dev-only', 'unreachable')) {
$external = $name -notlike 'oa-*'
$root = if ($external) { "registry/src/test/$name-1.0.0" } else { "workspace/$name" }
$manifest = Write-Fixture "$root/Cargo.toml" "[package]`nname = `"$name`"`nversion = `"1.0.0`""
@@ -40,10 +40,13 @@ foreach ($name in @('oa-desktop', 'oa-server', 'normal', 'build', 'dev-only', 'u
$deps += [pscustomobject]@{ pkg = $pair[0]; dep_kinds = @([pscustomobject]@{ kind = $pair[1] }) }
}
}
if ($name -eq 'oa-cli') {
$deps += [pscustomobject]@{ pkg = 'normal'; dep_kinds = @([pscustomobject]@{ kind = $null }) }
}
$nodes += [pscustomobject]@{ id = $id; deps = $deps; features = @() }
}
$null = Write-Fixture 'Cargo.lock' ($lock -join "`n")
$script:FixtureMetadata = [pscustomobject]@{ packages = $packages; workspace_members = @('oa-desktop', 'oa-server'); resolve = [pscustomobject]@{ nodes = $nodes } }
$script:FixtureMetadata = [pscustomobject]@{ packages = $packages; workspace_members = @('oa-desktop', 'oa-server', 'oa-cli'); resolve = [pscustomobject]@{ nodes = $nodes } }
$script:FixtureTarget = 'x86_64-pc-windows-msvc'
function cargo {
if (($args -join ' ') -notmatch '--locked --offline' -or ($args -join ' ') -notmatch "--filter-platform $script:FixtureTarget") { throw 'Unexpected Cargo command' }
@@ -60,6 +63,12 @@ function rustc {
$manifestPath = Write-Fixture 'supplements/manifest.json' '{"packages":[],"assets":[],"runtimes":[]}'
function Collect { Get-WindowsNotices $fixture x86_64-pc-windows-msvc $manifestPath '' }
function Assert([bool]$Condition, [string]$Message) { if (-not $Condition) { throw $Message } }
$defaultRoots = (Collect).Inventory.root_packages -join ','
Assert ($defaultRoots -eq 'oa-desktop,oa-server') 'Desktop/server default roots changed'
$cliOnly = Get-ArcadiaNotices $fixture x86_64-pc-windows-msvc $manifestPath '' @('oa-cli')
Assert (($cliOnly.Inventory.root_packages -join ',') -eq 'oa-cli') 'CLI root declaration missing'
Assert ((@($cliOnly.Inventory.packages.name | Sort-Object) -join ',') -eq 'normal,oa-cli') 'CLI notice graph leaked desktop/server/dev dependencies'
try { $null = Get-ArcadiaNotices $fixture x86_64-pc-windows-msvc $manifestPath '' @('unknown'); throw 'Expected root refusal' } catch { Assert ($_.ToString() -match 'Notice roots must') 'Unknown root was not rejected before metadata' }
function Refuses([string]$Pattern) {
try { $null = Collect; throw 'Expected preflight refusal' } catch {
if ($_.Exception.Message -notmatch $Pattern) { throw }
+1 -1
View File
@@ -1,7 +1,7 @@
#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') }).Count -gt 0) { throw 'Notice roots must be oa-desktop and/or 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)