Add portable Linux packaging and verify distribution notices

This commit is contained in:
2026-09-10 11:38:10 -07:00
parent a70de55ecd
commit b98f135799
24 changed files with 1374 additions and 141 deletions
+369
View File
@@ -0,0 +1,369 @@
#!/usr/bin/env python3
"""Stdlib-only prebuilt artifact inspection, document closure and tar verification.
Never builds, downloads, launches an input executable, or overwrites an archive.
"""
import argparse
import gzip
import hashlib
import html
from html.parser import HTMLParser
import json
from pathlib import Path, PurePosixPath
import re
import stat
import struct
import subprocess
import sys
import tarfile
from urllib.parse import unquote, urlsplit
def digest(path):
with Path(path).open('rb') as stream:
return hashlib.file_digest(stream, 'sha256').hexdigest()
def ordinary(path):
path = Path(path).absolute()
if not path.is_file():
raise ValueError(f'Expected ordinary file: {path}')
reject_reparse(path)
return path
def reject_reparse(path):
for candidate in (path, *path.parents):
info = candidate.lstat()
if stat.S_ISLNK(info.st_mode) or getattr(info, 'st_file_attributes', 0) & getattr(stat, 'FILE_ATTRIBUTE_REPARSE_POINT', 0):
raise ValueError(f'Symlink/reparse ancestor not permitted in package input: {candidate}')
def local_links(path):
text = path.read_text(encoding='utf-8-sig')
if path.suffix.lower() == '.md':
# Commands/example syntax are not rendered Markdown links.
text = re.sub(r'(?ms)^\s*(`{3,}|~{3,}).*?^\s*\1\s*$', '', text)
text = re.sub(r'`[^`\n]*`', '', text)
links = re.findall(r'!?\[[^\]\n]*\]\(\s*(<[^>]*>|[^\s)]+)(?:\s+[^)]*)?\)', text)
links += re.findall(r'(?m)^\s*\[[^]\n]+\]:\s*(<[^>]*>|\S+)', text)
elif path.suffix.lower() == '.rst':
links = re.findall(r'`[^`\n]*<([^>]+)>`_', text)
links += re.findall(r'(?m)^\.\.\s+_[^:\n]+:\s*(\S+)', text)
elif path.suffix.lower() in ('.html', '.htm'):
links = []
else:
return []
class Links(HTMLParser):
def handle_starttag(self, tag, attrs):
if tag == 'base' and any(name == 'href' for name, _ in attrs):
raise ValueError(f'HTML base URL requires explicit document-closure support: {path}')
links.extend(value for name, value in attrs if name in ('href', 'src') and value)
Links().feed(text)
return [html.unescape(link.strip('<>')) for link in links]
def linked_path(origin, link):
parsed = urlsplit(link)
if parsed.scheme or parsed.netloc or not parsed.path:
return None
decoded = unquote(parsed.path)
if '\\' in decoded or decoded.startswith('/'):
raise ValueError(f'Nonportable document link: {origin}: {link}')
parts = []
for part in (PurePosixPath(origin).parent / decoded).parts:
if part == '..':
if not parts:
raise ValueError(f'Document link escapes package: {origin}: {link}')
parts.pop()
elif part not in ('', '.'):
parts.append(part)
return '/'.join(parts)
def document_inputs(repository, platform):
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/'))
# 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'}
def repository_file(name):
candidate = ordinary(repository / name)
candidate.resolve().relative_to(repository)
return candidate
sources = {'README.md': repository_file('scripts/package-readme.md')}
for name in roots:
sources[name] = repository_file(name)
queue = list(sources)
while queue:
origin = queue.pop()
for link in local_links(sources[origin]):
relative = linked_path(origin, link)
if relative is None or relative in sources:
continue
if relative not in permitted:
raise ValueError(f'Untracked/missing package document link: {origin}: {link}')
candidate = repository_file(relative)
sources[relative] = candidate
queue.append(relative)
return {'files': [{'path': name, 'source': str(source)}
for name, source in sorted(sources.items())]}
def check_documents(stage):
stage = Path(stage).resolve()
count = 0
for source in sorted(stage.rglob('*')):
if not source.is_file() or source.suffix.lower() not in ('.md', '.rst', '.html', '.htm'):
continue
relative = source.relative_to(stage).as_posix()
# Third-party/Rust source notices can contain their own source-tree links;
# preserve those upstream texts verbatim, rather than rewriting licenses.
if relative.startswith('licenses/'):
continue
for link in local_links(source):
target = linked_path(relative, link)
if target is not None:
ordinary(stage / target)
count += 1
return {'checked_local_document_links': count}
def inspect_elf(path):
path = ordinary(path)
with path.open('rb') as stream:
data = stream.read()
if len(data) < 64 or data[:6] != b'\x7fELF\x02\x01':
raise ValueError(f'Expected ELF64 little-endian input: {path}')
kind, machine = struct.unpack_from('<HH', data, 16)
if machine != 62 or kind not in (2, 3):
raise ValueError(f'Expected x86-64 executable/PIE ELF: {path}')
phoff = struct.unpack_from('<Q', data, 32)[0]
phsize, phnum = struct.unpack_from('<HH', data, 54)
if phsize != 56 or not 0 < phnum <= 1024 or phoff + phsize * phnum > len(data):
raise ValueError('Invalid/bounded ELF program table')
programs = [struct.unpack_from('<IIQQQQQQ', data, phoff + i * phsize) for i in range(phnum)]
for _, _, offset, _, _, size, _, _ in programs:
if offset + size > len(data):
raise ValueError('ELF segment escapes file')
def string(offset, end):
if not 0 <= offset < end <= len(data):
raise ValueError('Invalid ELF string range')
stop = data.find(b'\0', offset, min(end, offset + 4096))
if stop < 0:
raise ValueError('Unterminated/bounded ELF string')
return data[offset:stop].decode('ascii')
interpreter = None
dynamic = []
for ptype, _, offset, _, _, size, _, _ in programs:
if ptype == 3:
interpreter = string(offset, offset + size)
if ptype == 2:
if size // 16 > 65536 or size % 16:
raise ValueError('Invalid/bounded ELF dynamic table')
for entry in range(offset, offset + size, 16):
tag, value = struct.unpack_from('<QQ', data, entry)
if tag == 0:
break
dynamic.append((tag, value))
else:
raise ValueError('ELF dynamic table lacks terminator')
tags = dict(dynamic)
needed, paths = [], []
if dynamic:
address, string_size = tags.get(5), tags.get(10)
if address is None or string_size is None:
raise ValueError('ELF lacks dynamic string table')
offsets = [offset + address - vaddr for ptype, _, offset, vaddr, _, size, _, _ in programs
if ptype == 1 and vaddr <= address and address + string_size <= vaddr + size]
if len(offsets) != 1:
raise ValueError('ELF dynamic string table is not uniquely mapped')
for tag, value in dynamic:
if tag in (1, 15, 29):
value = string(offsets[0] + value, offsets[0] + string_size)
(needed if tag == 1 else paths).append(value)
if interpreter != '/lib64/ld-linux-x86-64.so.2':
raise ValueError('Supported Linux package requires the x86-64 glibc dynamic loader')
if paths:
raise ValueError(f'ELF RPATH/RUNPATH requires explicit redistribution review: {paths}')
system = {'libc.so.6', 'libm.so.6', 'libpthread.so.0', 'libdl.so.2', 'librt.so.1',
'ld-linux-x86-64.so.2', 'libgcc_s.so.1', 'libstdc++.so.6'}
if set(needed) - system:
raise ValueError(f'Unreviewed Linux dynamic dependencies: {sorted(set(needed) - system)}')
versions = sorted(set(match.decode() for match in re.findall(rb'GLIBC_[0-9]+(?:\.[0-9]+)+', data)))
return {'sha256': hashlib.sha256(data).hexdigest(), 'bytes': len(data),
'format': 'ELF64 little-endian x86-64', 'interpreter': interpreter,
'needed': sorted(set(needed)), 'rpath_runpath': paths,
'observed_glibc_version_strings': versions,
'scope': 'Static ELF inspection only. Dynamic loads, display/graphics/portal dependencies and runtime compatibility require native checks.'}
def stage_files(stage):
stage = Path(stage).absolute()
reject_reparse(stage)
stage = stage.resolve()
files = {}
for path in sorted(stage.rglob('*')):
info = path.lstat()
if stat.S_ISLNK(info.st_mode) or getattr(info, 'st_file_attributes', 0) & getattr(stat, 'FILE_ATTRIBUTE_REPARSE_POINT', 0):
raise ValueError(f'Symlink not permitted in package: {path}')
if path.is_file():
relative = path.relative_to(stage).as_posix()
if any(c in relative for c in ('\n', '\r', '\\')):
raise ValueError('Unsafe archive name')
files[relative] = path
if not files:
raise ValueError('Empty package')
if len({name.casefold() for name in files}) != len(files):
raise ValueError('Case-colliding package names')
return files
def make_archive(stage, archive, package_name):
if not re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9._-]{0,159}', package_name):
raise ValueError('Unsafe package name')
files = stage_files(stage)
expected = {name: digest(path) for name, path in files.items()}
with Path(archive).open('xb') as output:
with gzip.GzipFile(filename='', mode='wb', fileobj=output, mtime=0) as compressed:
with tarfile.open(fileobj=compressed, mode='w', format=tarfile.PAX_FORMAT) as tar:
for relative, path in files.items():
info = tarfile.TarInfo(f'{package_name}/{relative}')
info.size = path.stat().st_size
info.mode = 0o755 if relative in ('arcadia-studio', 'arcadia-server') else 0o644
info.mtime = 0
with path.open('rb') as source:
tar.addfile(info, source)
seen = set()
with tarfile.open(archive, 'r:gz') as tar:
for entry in tar:
prefix = package_name + '/'
if not entry.isfile() or not entry.name.startswith(prefix):
raise ValueError('Unexpected archive entry')
relative = entry.name[len(prefix):]
if relative in seen or relative not in expected:
raise ValueError('Duplicate/extra archive entry')
seen.add(relative)
stream = tar.extractfile(entry)
if hashlib.file_digest(stream, 'sha256').hexdigest() != expected[relative]:
raise ValueError(f'Archive readback mismatch: {relative}')
mode = 0o755 if relative in ('arcadia-studio', 'arcadia-server') else 0o644
if entry.mode != mode or entry.uid or entry.gid or entry.mtime:
raise ValueError('Archive permissions/metadata mismatch')
if seen != set(expected) or set(stage_files(stage)) != set(expected) or any(digest(path) != expected[name] for name, path in files.items()):
raise ValueError('Stage changed while archiving or archive incomplete')
return {'archive': str(Path(archive).resolve()), 'sha256': digest(archive), 'files': len(seen),
'verification': 'All entries read back, hashes/permissions/metadata checked; stage unchanged.'}
def verify_notices(directory, repository, target):
directory, repository = Path(directory).resolve(), Path(repository).resolve()
inventory = json.loads(ordinary(directory / 'dependency-notices.json').read_text(encoding='utf-8-sig'))
if inventory['target'] != target or inventory['root_packages'] != ['oa-server']:
raise ValueError('Container requires server-only notices for its exact Linux target')
if inventory['cargo_lock_sha256'] != digest(repository / 'Cargo.lock'):
raise ValueError('Container notice Cargo.lock mismatch')
common = ['Cargo.toml', 'Cargo.lock', 'LICENSE', 'docs/openapi.json']
common += [path.relative_to(repository).as_posix() for path in sorted((repository / 'build').rglob('*')) if path.is_file()]
if 'build/windows_manifest.rs' not in common:
raise ValueError('Container shared build manifest input is missing')
actual_common = {name: digest(ordinary(repository / name)) for name in common}
if actual_common != inventory['workspace_build_inputs_sha256']:
raise ValueError('Container shared build input set/hashes differ from notice inventory')
current = subprocess.check_output(['rustc', '-Vv'], text=True).splitlines()
for label in ('release:', 'commit-hash:'):
if [s for s in current if s.startswith(label)] != [s for s in inventory['rust']['version'] if s.startswith(label)]:
raise ValueError(f'Container notice compiler mismatch: {label}')
libdir = Path(subprocess.check_output(['rustc', '--print', 'target-libdir', '--target', target], text=True).strip())
actual = {path.name: digest(path) for path in sorted(libdir.iterdir()) if path.is_file()}
if actual != inventory['rust']['target_libraries_sha256']:
raise ValueError('Container notice target-library hash mismatch')
checksums = ordinary(directory / 'SHA256SUMS.txt').read_text(encoding='utf-8-sig').splitlines()
expected = {}
for line in checksums:
checksum, name = line.split(' ', 1)
parts = PurePosixPath(name)
if parts.is_absolute() or '..' in parts.parts or name in expected or '\\' in name:
raise ValueError('Unsafe/duplicate notice manifest entry')
if not re.fullmatch('[0-9a-f]{64}', checksum):
raise ValueError('Malformed notice checksum')
expected[name] = checksum
actual_files = stage_files(directory)
if set(actual_files) != set(expected) | {'SHA256SUMS.txt'}:
raise ValueError('Notice bundle inventory differs from checksum manifest')
for name, checksum in expected.items():
if digest(ordinary(directory / name)) != checksum:
raise ValueError(f'Notice bundle file mismatch: {name}')
def check_notice_references(value):
if isinstance(value, dict):
if 'path' in value and 'sha256' in value and value['path'].startswith('licenses/'):
if expected.get(value['path']) != value['sha256']:
raise ValueError(f'Inventory notice reference differs from bundle: {value["path"]}')
for child in value.values():
check_notice_references(child)
elif isinstance(value, list):
for child in value:
check_notice_references(child)
check_notice_references(inventory)
for package in inventory['packages']:
if not package['source']:
if package['name'] not in ('oa-core', 'oa-server', 'oa-desktop'):
raise ValueError('Unexpected workspace package in container notices')
root = repository / 'crates' / package['name']
actual_sources = {name: digest(path) for name, path in stage_files(root).items()
if not name.startswith('target/') and path.name != '.cargo-ok'}
if actual_sources != package['local_source_sha256']:
raise ValueError(f'Container workspace differs from notice source set/hashes: {package["name"]}')
for name, checksum in package['local_source_sha256'].items():
if PurePosixPath(name).is_absolute() or '..' in PurePosixPath(name).parts or '\\' in name:
raise ValueError('Unsafe workspace source inventory path')
if digest(ordinary(root / name)) != checksum:
raise ValueError(f'Container workspace differs from notice source: {package["name"]}/{name}')
return {'container_notice_preflight': 'passed', 'target': target}
def main():
parser = argparse.ArgumentParser(description=__doc__)
commands = parser.add_subparsers(dest='command', required=True)
docs = commands.add_parser('docs')
docs.add_argument('--repository', required=True)
docs.add_argument('--platform', choices=['windows', 'linux'], required=True)
check = commands.add_parser('check-docs')
check.add_argument('--stage', required=True)
elf = commands.add_parser('elf')
elf.add_argument('path')
archive = commands.add_parser('archive')
archive.add_argument('--stage', required=True)
archive.add_argument('--archive', required=True)
archive.add_argument('--name', required=True)
notices = commands.add_parser('verify-notices')
notices.add_argument('--directory', required=True)
notices.add_argument('--repository', required=True)
notices.add_argument('--target', required=True)
args = parser.parse_args()
if args.command == 'docs':
result = document_inputs(args.repository, args.platform)
elif args.command == 'check-docs':
result = check_documents(args.stage)
elif args.command == 'elf':
result = inspect_elf(args.path)
elif args.command == 'archive':
result = make_archive(args.stage, args.archive, args.name)
else:
result = verify_notices(args.directory, args.repository, args.target)
print(json.dumps(result, ensure_ascii=False))
if __name__ == '__main__':
try:
main()
except (ValueError, OSError, KeyError, subprocess.CalledProcessError, struct.error) as error:
print(f'Packaging failed: {error}', file=sys.stderr)
sys.exit(1)
+76
View File
@@ -0,0 +1,76 @@
#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)
}
+98
View File
@@ -0,0 +1,98 @@
#Requires -Version 5.1
# Package already-built x86-64 glibc inputs or prepare strict server notices.
[CmdletBinding(DefaultParameterSetName = 'Package')]
param(
[ValidateSet('x86_64-unknown-linux-gnu')]
[string]$Target = 'x86_64-unknown-linux-gnu',
[Parameter(Mandatory = $true, ParameterSetName = 'Package')]
[ValidatePattern('^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$')]
[string]$Version,
[Parameter(Mandatory = $true, ParameterSetName = 'Package')]
[string]$BinaryDirectory,
[Parameter(Mandatory = $true, ParameterSetName = 'Notices')]
[switch]$ServerNoticesOnly,
[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 '..'))
$encoding = New-Object Text.UTF8Encoding($false)
. (Join-Path $PSScriptRoot 'package-common.ps1')
. (Join-Path $PSScriptRoot 'windows-notices.ps1')
$inputs = [ordered]@{}
$inspectedHashes = @{}
$imports = [ordered]@{}
$evidence = $null
if (-not $ServerNoticesOnly) {
$binaryHashes = [ordered]@{}
foreach ($name in @('arcadia-studio', 'arcadia-server')) {
$path = [IO.Path]::GetFullPath((Join-Path $BinaryDirectory $name))
$elf = Invoke-PackageHelper $Python @('elf', $path)
$inputs[$name] = $path
$imports[$name] = $elf
$inspectedHashes[$name] = $elf.sha256
$binaryHashes[$name] = $elf.sha256
}
$evidence = Read-PackageBuildEvidence $BuildEvidence $Target $binaryHashes
$documentation = Get-PackageDocumentation $repository 'linux' $Python
foreach ($name in $documentation.Keys) { $inputs[$name] = $documentation[$name] }
$notices = Get-ArcadiaNotices $repository $Target $NoticeManifest ''
} else {
if ($BuildEvidence) { throw '-BuildEvidence applies to a binary package, not a notice-only bundle.' }
$notices = Get-ArcadiaNotices $repository $Target $NoticeManifest '' @('oa-server')
$inputs['LICENSE'] = Join-Path $repository 'LICENSE'
}
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 = if ($ServerNoticesOnly) { 'server-notices' } else { "ArcadiaStudio-$Version-$Target" }
$stage = Join-Path $output $packageName
$archive = "$stage.tar.gz"
$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/output root: $path" }
}
foreach ($directory in @('docs', 'examples', '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
Write-PackageNotices $stage $notices
if (-not $ServerNoticesOnly) {
$metadata = [ordered]@{
version = $Version; target = $Target
acceptance = 'Packaging integrity only; current-tree native Linux acceptance required.'
build = 'Prebuilt inputs; no build profile, source identity or compiler inferred.'
build_evidence = $evidence; imports = $imports; input_sha256 = $inputHashes
runtime = 'Ubuntu 24.04 x86-64 is the validation baseline. Direct ELF dependencies only are inspected; graphics/window/portal dynamic loads must be verified on the actual runtime.'
}
[IO.File]::WriteAllText((Join-Path $stage 'package.json'), ($metadata | ConvertTo-Json -Depth 14) + "`n", $encoding)
[IO.File]::WriteAllText((Join-Path $stage 'START-HERE.txt'), "Arcadia Studio portable Linux bundle`nRun ./arcadia-studio. Keep examples/release1 beside the executable.`nRead README.md and docs/linux-packaging.md. The optional server is ./arcadia-server.`nSource checkout required for Cargo/Docker instructions. Packaging is not runtime acceptance.`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
if ($ServerNoticesOnly) {
[pscustomobject]@{ Directory = $stage; Target = $Target; Files = @(Get-ChildItem -LiteralPath $stage -Recurse -File).Count; Scope = 'Server-only notices; no application binary or runtime claim.' }
} else {
$result = Invoke-PackageHelper $Python @('archive', '--stage', $stage, '--archive', $archive, '--name', $packageName)
[IO.File]::WriteAllText($checksum, "$($result.sha256) $packageName.tar.gz`n", $encoding)
[pscustomobject]@{ Directory = $stage; Archive = $archive; SHA256 = $result.sha256; Files = $result.files; NativeAcceptance = 'Pending' }
}
} finally {
$lock.Dispose()
# Claims and partial outputs intentionally remain. Never overwrite a failed attempt.
}
+11
View File
@@ -0,0 +1,11 @@
# Arcadia Studio portable bundle
Run `arcadia-studio.exe` on Windows or `./arcadia-studio` on Linux. Keep this folder together; Home's water-pressure walkthrough uses the complete `examples/release1` folder beside the executable. The application includes searchable offline Help. No Rust installation or server is needed for local modeling.
Start with Home → New or the water-pressure walkthrough. Use the built-in guide for the controls in this binary. The [worked example](examples/release1/README.md), [language and graphical editing contract](docs/language.rst) and [REST contract](docs/api.rst) are included offline. HTML versions are in `docs/language.html` and `docs/api.html`.
The optional `arcadia-server` executable provides programmatic model access. See [server configuration](docs/server.md). Keep its data directory outside this application folder so replacing a bundle does not replace stored models. Cargo and Docker commands in the included reference documentation require a separate source checkout and its build prerequisites; this binary bundle is not that checkout.
Read the included platform packaging guide for runtime prerequisites and clean-environment checks. `SHA256SUMS.txt` inventories package files, and the adjacent archive checksum covers the complete archive. `package.json` records the supplied binary hashes, inspected dependencies, and any explicitly supplied build evidence. `dependency-notices.json`, `THIRD-PARTY-NOTICES.txt` and `licenses/` retain dependency and runtime notices.
Packaging integrity establishes what was delivered. It does not establish current release acceptance, supported graphics on this machine, or that caller-supplied binaries were built from the accompanying source documentation. Consult the binary-specific build and native verification evidence supplied by the distributor; a development preview remains a development preview.
+15 -22
View File
@@ -11,6 +11,8 @@ param(
[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')
)
@@ -19,6 +21,7 @@ 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')
function Require-File([string]$Path) {
if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { throw "Required file missing: $Path" }
@@ -121,21 +124,11 @@ foreach ($name in @($inputs.Keys | Where-Object { $_ -match '\.(exe|dll)$' })) {
}
}
foreach ($name in @('README.md', 'LICENSE')) { $inputs[$name] = Require-File (Join-Path $repository $name) }
$tracked = @(& git -C $repository -c core.quotepath=false ls-files -- docs examples deployment)
if ($LASTEXITCODE -ne 0 -or $tracked.Count -eq 0) { throw 'A Git checkout and tracked docs/examples/deployment inputs are required.' }
foreach ($relative in $tracked) {
$source = Require-File (Join-Path $repository $relative)
if (((Get-Item -LiteralPath $source).Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) {
throw "Package input must not be a symlink: $source"
}
$inputs[$relative] = $source
}
# Explicitly require the packaging guide, including before its first commit.
$inputs['docs/windows-packaging.md'] = Require-File (Join-Path $repository 'docs/windows-packaging.md')
foreach ($name in @('release-current-coverage-20260906.md', 'release-save-as-runtime-packaging.md')) {
$inputs["harness/$name"] = Require-File (Join-Path $repository "harness/$name")
}
$binaryHashes = [ordered]@{}
foreach ($name in @('arcadia-studio.exe', 'arcadia-server.exe')) { $binaryHashes[$name] = $inspectedHashes[$name] }
$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')
@@ -167,6 +160,7 @@ 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()
@@ -183,13 +177,10 @@ try {
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.'
imports = $imports; input_sha256 = $inputHashes
build_evidence = $evidence; imports = $imports; input_sha256 = $inputHashes
}
[IO.File]::WriteAllText((Join-Path $stage 'package.json'), ($metadata | ConvertTo-Json -Depth 6) + "`n", $utf8)
[IO.File]::WriteAllText((Join-Path $stage 'dependency-notices.json'), ($notices.Inventory | ConvertTo-Json -Depth 14) + "`n", $utf8)
$noticeIndex = @('Third-party notice inventory', '', 'Exact package versions, license expressions, source hashes and notice paths:', 'dependency-notices.json', '', 'License texts and embedded-asset notices are retained under licenses/.', 'Rust distribution library/copyright reports are retained under licenses/rust/.', 'Collection is conservative; it does not establish binary/source equivalence or constitute a universal license audit.', '')
foreach ($package in $notices.Inventory.packages) { $noticeIndex += "$($package.name) $($package.version) -- $($package.license) -- licenses/dependencies/$($package.name)-$($package.version)/" }
[IO.File]::WriteAllText((Join-Path $stage 'THIRD-PARTY-NOTICES.txt'), ($noticeIndex -join "`n") + "`n", $utf8)
[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
@@ -201,9 +192,11 @@ 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 harness/release-current-coverage-20260906.md for limitations.
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('\', '/')
+211
View File
@@ -0,0 +1,211 @@
#!/usr/bin/env python3
"""Packaging invariants; stdlib synthetic files, no Cargo, builds or GUI."""
import importlib.util
import json
from pathlib import Path
import struct
import tarfile
import tempfile
import unittest
from unittest.mock import patch
spec = importlib.util.spec_from_file_location('packaging', Path(__file__).with_name('package-artifacts.py'))
packaging = importlib.util.module_from_spec(spec)
spec.loader.exec_module(packaging)
class PackageContracts(unittest.TestCase):
def setUp(self):
self.temporary = tempfile.TemporaryDirectory(prefix='arcadia-packaging-test-')
self.root = Path(self.temporary.name)
def tearDown(self):
self.temporary.cleanup()
def file(self, name, data=b'test'):
path = self.root / name
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(data)
return path
def elf(self, needed=b'libc.so.6', interpreter=b'/lib64/ld-linux-x86-64.so.2', runpath=False):
# One load maps the file, followed by interpreter and dynamic segments.
data = bytearray(1024)
data[:6] = b'\x7fELF\x02\x01'
struct.pack_into('<HH', data, 16, 3, 62)
struct.pack_into('<Q', data, 32, 64)
struct.pack_into('<HH', data, 54, 56, 3)
data[300:300 + len(interpreter) + 1] = interpreter + b'\0'
strings = b'\0' + needed + b'\0'
data[600:600 + len(strings)] = strings
struct.pack_into('<IIQQQQQQ', data, 64, 1, 4, 0, 0x400000, 0, len(data), len(data), 4096)
struct.pack_into('<IIQQQQQQ', data, 120, 3, 4, 300, 0x40012c, 0, len(interpreter) + 1, len(interpreter) + 1, 1)
entries = [(5, 0x400000 + 600), (10, len(strings)), (1, 1)]
if runpath:
entries.append((29, 1))
entries.append((0, 0))
struct.pack_into('<IIQQQQQQ', data, 176, 2, 4, 400, 0x400190, 0, len(entries) * 16, len(entries) * 16, 8)
for i, entry in enumerate(entries):
struct.pack_into('<QQ', data, 400 + i * 16, *entry)
return self.file('input.elf', data)
def test_tar_is_deterministic_with_executable_modes_and_unicode_paths(self):
self.file('stage/arcadia-studio', b'not executable in this test')
self.file('stage/arcadia-server', b'another inert fixture')
self.file('stage/examples/temperature delta.txt', 'Δ\n'.encode())
first = packaging.make_archive(self.root / 'stage', self.root / 'a.tar.gz', 'fixture')
second = packaging.make_archive(self.root / 'stage', self.root / 'b.tar.gz', 'fixture')
self.assertEqual(first['sha256'], second['sha256'])
with tarfile.open(self.root / 'a.tar.gz') as archive:
self.assertEqual(archive.getmember('fixture/arcadia-studio').mode, 0o755)
self.assertEqual(archive.getmember('fixture/examples/temperature delta.txt').mode, 0o644)
self.assertEqual(len(archive.getmembers()), 3)
def test_archive_refuses_overwrite_and_case_collisions(self):
self.file('stage/a', b'first')
archive = self.file('archive.tar.gz', b'preserve')
with self.assertRaises(FileExistsError):
packaging.make_archive(self.root / 'stage', archive, 'fixture')
self.assertEqual(archive.read_bytes(), b'preserve')
with patch.object(packaging, 'stage_files', return_value={}):
with self.assertRaises(ValueError):
packaging.make_archive(self.root / 'stage', archive, '../escape')
def test_archive_refuses_a_file_added_during_readback(self):
self.file('stage/a', b'first')
original = packaging.tarfile.open
def open_and_add(*args, **kwargs):
result = original(*args, **kwargs)
if len(args) > 1 and args[1] == 'r:gz':
self.file('stage/late.txt', b'late')
return result
with patch.object(packaging.tarfile, 'open', side_effect=open_and_add):
with self.assertRaisesRegex(ValueError, 'Stage changed'):
packaging.make_archive(self.root / 'stage', self.root / 'archive.tar.gz', 'fixture')
def test_input_rejects_a_reparse_ancestor(self):
source = self.file('ordinary/file.txt')
original = Path.lstat
def reparse(path, **kwargs):
if path == source.parent:
result = original(path, **kwargs)
return type('ReparseStat', (), {'st_mode': result.st_mode, 'st_file_attributes': 1024})()
return original(path, **kwargs)
with patch.object(Path, 'lstat', reparse), patch.object(packaging.stat, 'FILE_ATTRIBUTE_REPARSE_POINT', 1024, create=True):
with self.assertRaisesRegex(ValueError, 'reparse ancestor'):
packaging.ordinary(source)
def test_local_document_links_are_complete_and_cannot_escape(self):
self.file('docs/README.md', b'[guide](guide%20one.md#section)\n[web](https://example.com)\n')
self.file('docs/guide one.md', b'# section\n')
self.assertEqual(packaging.check_documents(self.root / 'docs')['checked_local_document_links'], 1)
self.file('docs/README.md', b'[missing](missing.md)')
with self.assertRaises(ValueError):
packaging.check_documents(self.root / 'docs')
self.file('docs/README.md', b'[escape](../outside.md)')
with self.assertRaises(ValueError):
packaging.check_documents(self.root / 'docs')
def test_rst_directives_and_fenced_examples_are_not_links(self):
self.file('docs/a.rst', b'.. code-block:: rust\n\n sample\n')
self.file('docs/a.md', b'```text\n[example](absent.md)\n```\n`[sample](absent.md)`\n')
self.assertEqual(packaging.check_documents(self.root / 'docs')['checked_local_document_links'], 0)
def test_html_attribute_case_spacing_and_unquoted_links_are_checked(self):
self.file('docs/a.html', b'<A HREF = "missing.html">Missing</A>')
with self.assertRaises(ValueError):
packaging.check_documents(self.root / 'docs')
self.file('docs/missing.html', b'ok')
self.file('docs/a.html', b'<a href=missing.html>Present</a>')
self.assertEqual(packaging.check_documents(self.root / 'docs')['checked_local_document_links'], 1)
def test_elf_accepts_reviewed_glibc_and_refuses_unknown_runtime(self):
self.assertEqual(packaging.inspect_elf(self.elf())['needed'], ['libc.so.6'])
with self.assertRaisesRegex(ValueError, 'Unreviewed'):
packaging.inspect_elf(self.elf(needed=b'private-runtime.so'))
with self.assertRaisesRegex(ValueError, 'RPATH'):
packaging.inspect_elf(self.elf(runpath=True))
def test_elf_rejects_wrong_architecture_truncation_and_loader(self):
with self.assertRaisesRegex(ValueError, 'glibc dynamic loader'):
packaging.inspect_elf(self.elf(interpreter=b'/lib/ld-musl-x86_64.so.1'))
elf = self.elf()
data = bytearray(elf.read_bytes())
struct.pack_into('<H', data, 18, 183)
elf.write_bytes(data)
with self.assertRaisesRegex(ValueError, 'x86-64'):
packaging.inspect_elf(elf)
elf.write_bytes(b'\x7fELF')
with self.assertRaisesRegex(ValueError, 'ELF64'):
packaging.inspect_elf(elf)
def test_container_refuses_wrong_target_roots_and_stale_lock_before_tools(self):
lock = self.file('repo/Cargo.lock', b'lock')
inventory = {'target': 'x86_64-pc-windows-msvc', 'root_packages': ['oa-server'],
'cargo_lock_sha256': packaging.digest(lock)}
self.file('notices/dependency-notices.json', json.dumps(inventory).encode())
with patch.object(packaging.subprocess, 'check_output', side_effect=AssertionError('must not run')):
with self.assertRaisesRegex(ValueError, 'server-only'):
packaging.verify_notices(self.root / 'notices', self.root / 'repo', 'x86_64-unknown-linux-gnu')
inventory['target'] = 'x86_64-unknown-linux-gnu'
inventory['cargo_lock_sha256'] = '0' * 64
self.file('notices/dependency-notices.json', json.dumps(inventory).encode())
with self.assertRaisesRegex(ValueError, 'Cargo.lock mismatch'):
packaging.verify_notices(self.root / 'notices', self.root / 'repo', inventory['target'])
def test_container_verifies_actual_notices_compiler_libraries_and_sources(self):
lock = self.file('repo/Cargo.lock', b'lock')
source = self.file('repo/crates/oa-server/src/main.rs', b'source')
self.file('repo/Cargo.toml', b'workspace')
self.file('repo/LICENSE', b'license')
self.file('repo/docs/openapi.json', b'{}')
self.file('repo/build/windows_manifest.rs', b'build script')
library = self.file('rust/lib/libstd.rlib', b'rust runtime')
notice = self.file('notices/licenses/example.txt', b'upstream notice')
version = ['release: 1.90.0', 'commit-hash: example']
inventory = {
'target': 'x86_64-unknown-linux-gnu', 'root_packages': ['oa-server'],
'cargo_lock_sha256': packaging.digest(lock),
'workspace_build_inputs_sha256': {name: packaging.digest(self.root / 'repo' / name) for name in
['Cargo.toml', 'Cargo.lock', 'LICENSE', 'docs/openapi.json', 'build/windows_manifest.rs']},
'rust': {'version': version, 'target_libraries_sha256': {library.name: packaging.digest(library)},
'notices': [{'path': 'licenses/example.txt', 'sha256': packaging.digest(notice)}]},
'packages': [{'name': 'oa-server', 'source': None,
'local_source_sha256': {'src/main.rs': packaging.digest(source)}}]}
self.file('notices/dependency-notices.json', json.dumps(inventory).encode())
def manifest():
lines = [f'{packaging.digest(path)} {path.relative_to(self.root / "notices").as_posix()}'
for path in sorted((self.root / 'notices').rglob('*'))
if path.is_file() and path.name != 'SHA256SUMS.txt']
self.file('notices/SHA256SUMS.txt', ('\n'.join(lines) + '\n').encode())
manifest()
def compiler(args, **_):
return '\n'.join(version) if args == ['rustc', '-Vv'] else str(library.parent)
with patch.object(packaging.subprocess, 'check_output', side_effect=compiler):
result = packaging.verify_notices(self.root / 'notices', self.root / 'repo', inventory['target'])
self.assertEqual(result['container_notice_preflight'], 'passed')
self.file('repo/crates/oa-server/src/added.rs', b'new module')
with self.assertRaisesRegex(ValueError, 'workspace differs'):
packaging.verify_notices(self.root / 'notices', self.root / 'repo', inventory['target'])
(self.root / 'repo/crates/oa-server/src/added.rs').unlink()
self.file('repo/docs/openapi.json', b'changed API')
with self.assertRaisesRegex(ValueError, 'shared build input'):
packaging.verify_notices(self.root / 'notices', self.root / 'repo', inventory['target'])
self.file('repo/docs/openapi.json', b'{}')
source.write_bytes(b'new source')
with self.assertRaisesRegex(ValueError, 'workspace differs'):
packaging.verify_notices(self.root / 'notices', self.root / 'repo', inventory['target'])
source.write_bytes(b'source')
notice.write_bytes(b'altered notice')
with self.assertRaisesRegex(ValueError, 'file mismatch'):
packaging.verify_notices(self.root / 'notices', self.root / 'repo', inventory['target'])
manifest()
with self.assertRaisesRegex(ValueError, 'Inventory notice reference'):
packaging.verify_notices(self.root / 'notices', self.root / 'repo', inventory['target'])
library.write_bytes(b'different compiler runtime')
with self.assertRaisesRegex(ValueError, 'target-library'):
packaging.verify_notices(self.root / 'notices', self.root / 'repo', inventory['target'])
if __name__ == '__main__':
unittest.main()
+22 -2
View File
@@ -14,6 +14,8 @@ function Write-Fixture([string]$Relative, [string]$Text) {
}
$null = Write-Fixture 'LICENSE' 'Synthetic workspace license for tests only'
$null = Write-Fixture 'Cargo.toml' '[workspace]'
$null = Write-Fixture 'docs/openapi.json' '{}'
$null = Write-Fixture 'build/windows_manifest.rs' '// synthetic shared build input'
$null = Write-Fixture 'rust/share/doc/rust/COPYRIGHT.html' 'Synthetic Rust distribution notice'
$null = Write-Fixture 'rust/share/doc/rust/COPYRIGHT-library.html' 'Synthetic Rust library notice'
$null = Write-Fixture 'rust/lib/fake.rlib' 'Synthetic target library'
@@ -42,8 +44,9 @@ foreach ($name in @('oa-desktop', 'oa-server', 'normal', 'build', 'dev-only', 'u
}
$null = Write-Fixture 'Cargo.lock' ($lock -join "`n")
$script:FixtureMetadata = [pscustomobject]@{ packages = $packages; workspace_members = @('oa-desktop', 'oa-server'); 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 x86_64-pc-windows-msvc') { throw 'Unexpected Cargo command' }
if (($args -join ' ') -notmatch '--locked --offline' -or ($args -join ' ') -notmatch "--filter-platform $script:FixtureTarget") { throw 'Unexpected Cargo command' }
$global:LASTEXITCODE = 0
$script:FixtureMetadata | ConvertTo-Json -Depth 12
}
@@ -66,6 +69,13 @@ $result = Collect
$names = @($result.Inventory.packages | ForEach-Object { $_.name })
Assert ($names.Count -eq 4 -and $names -contains 'normal' -and $names -contains 'build' -and $names -notcontains 'dev-only' -and $names -notcontains 'unreachable') 'Normal/build graph selection failed'
Assert ($result.Inventory.rust.notices.Count -eq 2) 'Rust reports missing'
Assert ($result.Inventory.workspace_build_inputs_sha256.Count -eq 5) 'Shared compiler input inventory missing'
$script:FixtureTarget = 'x86_64-unknown-linux-gnu'
$linuxNotices = Get-ArcadiaNotices $fixture $script:FixtureTarget $manifestPath '' @('oa-server')
Assert ($linuxNotices.Inventory.packages.Count -eq 1 -and $linuxNotices.Inventory.packages[0].name -eq 'oa-server') 'Server-only Linux notice graph includes desktop roots'
Assert ($linuxNotices.Inventory.root_packages.Count -eq 1 -and $linuxNotices.Inventory.root_packages[0] -eq 'oa-server' -and $null -eq $linuxNotices.Inventory.runtime) 'Linux target/root/runtime identity not retained'
try { $null = Get-ArcadiaNotices $fixture $script:FixtureTarget $manifestPath 'unexpected-runtime'; throw 'Expected runtime refusal' } catch { if ($_.Exception.Message -notmatch 'only to the gnullvm target') { throw } }
$script:FixtureTarget = 'x86_64-pc-windows-msvc'
$before = ($result.Inventory.packages | Where-Object name -eq normal).local_source_sha256['src/lib.rs']
$null = Write-Fixture 'registry/src/test/normal-1.0.0/src/lib.rs' '// locally changed source'
$after = ((Collect).Inventory.packages | Where-Object name -eq normal).local_source_sha256['src/lib.rs']
@@ -73,9 +83,19 @@ Assert ($before -ne $after) 'Local source hash was replaced by a cached checksum
$null = Write-Fixture 'registry/cache/test/normal-1.0.0.crate' 'Tampered archive'
Refuses 'Cached crate archive differs'
$null = Write-Fixture 'registry/cache/test/normal-1.0.0.crate' 'Synthetic cached archive normal'
$null = Write-Fixture 'registry/src/test/normal-1.0.0/src/lib.rs' "//! include_bytes!(`"../../dev/absent.ttf`")`n /// let font = include_bytes!(`"../../dev/absent.otf`");`n// include_bytes!(`"../../dev/absent.woff2`")`npub fn normal_code() {}"
Assert (@((Collect).Inventory.packages | Where-Object name -eq normal).Count -eq 1) 'Rustdoc font examples incorrectly treated as compiled assets'
$null = Write-Fixture 'registry/src/test/normal-1.0.0/src/lib.rs' 'static FONT: &[u8] = include_bytes!("../../dev/absent.ttf");'
Refuses 'Embedded font path escapes crate source root'
$null = Write-Fixture 'registry/src/test/normal-1.0.0/src/lib.rs' 'static FONT: &[u8] = include_bytes!("../font.ttf");'
$null = Write-Fixture 'registry/src/test/normal-1.0.0/font.ttf' 'Synthetic font bytes'
Refuses 'Embedded font requires exact notice mapping'
$fontNotice = Write-Fixture 'supplements/font-OFL.txt' 'Synthetic exact font notice'
$fontAsset = [ordered]@{ name = 'normal'; version = '1.0.0'; path = 'font.ttf'; sha256 = (Get-FileHash -LiteralPath (Join-Path $fixture 'registry/src/test/normal-1.0.0/font.ttf') -Algorithm SHA256).Hash.ToLowerInvariant(); license = 'OFL-1.1'; notices = @([ordered]@{ file = 'font-OFL.txt'; sha256 = (Get-FileHash -LiteralPath $fontNotice -Algorithm SHA256).Hash.ToLowerInvariant(); url = 'https://example.invalid/font-OFL.txt' }) }
$null = Write-Fixture 'supplements/manifest.json' ([ordered]@{ packages = @(); assets = @($fontAsset); runtimes = @() } | ConvertTo-Json -Depth 8)
Assert (@((Collect).Inventory.packages | Where-Object name -eq normal).Count -eq 1) 'Exact font mapping was rejected'
$null = Write-Fixture 'registry/src/test/normal-1.0.0/font.ttf' 'Different font bytes'
Refuses 'Embedded font requires exact notice mapping'
$null = Write-Fixture 'registry/src/test/normal-1.0.0/src/lib.rs' '// no font'
$pkg = $script:FixtureMetadata.packages | Where-Object name -eq normal
$pkg.source = 'git+https://example.invalid/test#123'
@@ -89,5 +109,5 @@ $bad.packages[0].notices[0].file = 'NOTICE.txt'
$null = Write-Fixture 'supplements/NOTICE.txt' 'Synthetic supplemental notice'
$null = Write-Fixture 'supplements/manifest.json' ($bad | ConvertTo-Json -Depth 8)
Refuses 'Supplement changed'
Write-Output 'PASS: normal/build selection, Rust reports, actual local source hashes, stale archive refusal, unmapped font refusal, unsupported source refusal, traversal refusal, altered supplement refusal.'
Write-Output 'PASS: normal/build and server-only Linux selection, cross-target runtime refusal, Rust reports, actual source hashes, stale archive refusal, doc-comment font exclusion, production font path/mapping refusals, exact font mapping and changed-byte refusal, unsupported source refusal, traversal refusal, altered supplement refusal.'
Write-Output "Synthetic fixture retained for inspection: $fixture"
+31 -5
View File
@@ -1,6 +1,8 @@
#Requires -Version 5.1
# Read-only dependency preflight. Does not build, download, or change a Cargo target.
function Get-WindowsNotices([string]$Repository, [string]$Target, [string]$NoticeManifest, [string]$LlvmMinGWRoot) {
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 ($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)
$sha = [Security.Cryptography.SHA256]::Create()
@@ -10,7 +12,8 @@ function Get-WindowsNotices([string]$Repository, [string]$Target, [string]$Notic
function Under-Root([string]$Root, [string]$Relative) {
$base = [IO.Path]::GetFullPath($Root).TrimEnd('\', '/') + [IO.Path]::DirectorySeparatorChar
$path = [IO.Path]::GetFullPath((Join-Path $base $Relative))
if (-not $path.StartsWith($base, [StringComparison]::OrdinalIgnoreCase)) { throw "Notice path escapes source root: $Relative" }
$comparison = if ([IO.Path]::DirectorySeparatorChar -eq '/') { [StringComparison]::Ordinal } else { [StringComparison]::OrdinalIgnoreCase }
if (-not $path.StartsWith($base, $comparison)) { throw "Notice path escapes source root: $Relative" }
if (-not [IO.File]::Exists($path)) { throw "Required notice/source missing: $path" }
return $path
}
@@ -39,6 +42,17 @@ function Get-WindowsNotices([string]$Repository, [string]$Target, [string]$Notic
}
$lock = Under-Root $Repository 'Cargo.lock'
$lockHash = Hash-File $lock
# Shared compiler inputs live outside individual crate directories.
$workspaceInputs = [ordered]@{}
foreach ($relative in @('Cargo.toml', 'Cargo.lock', 'LICENSE', 'docs/openapi.json')) {
$workspaceInputs[$relative] = Hash-File (Under-Root $Repository $relative)
}
foreach ($file in @(Get-ChildItem -LiteralPath (Join-Path $Repository 'build') -Recurse -File | Sort-Object FullName)) {
if (($file.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) { throw 'Shared build input reparse point requires review.' }
$relative = $file.FullName.Substring([IO.Path]::GetFullPath($Repository).TrimEnd('\', '/').Length + 1).Replace('\', '/')
$workspaceInputs[$relative] = Hash-File $file.FullName
}
if (-not $workspaceInputs.Contains('build/windows_manifest.rs')) { throw 'Shared Windows build manifest input is missing.' }
$locked = @{}
foreach ($block in ([IO.File]::ReadAllText($lock) -split '(?m)^\[\[package\]\]\s*$')) {
$fields = @{}
@@ -53,7 +67,7 @@ function Get-WindowsNotices([string]$Repository, [string]$Target, [string]$Notic
foreach ($package in $metadata.packages) { $packages[$package.id] = $package }
foreach ($node in $metadata.resolve.nodes) { $nodes[$node.id] = $node }
$queue = New-Object 'Collections.Generic.Queue[string]'
foreach ($name in @('oa-desktop', 'oa-server')) {
foreach ($name in @($RootPackages | Sort-Object -Unique)) {
$roots = @($metadata.packages | Where-Object { $_.name -eq $name -and $metadata.workspace_members -contains $_.id })
if ($roots.Count -ne 1) { throw "Expected one workspace package: $name" }
$queue.Enqueue($roots[0].id)
@@ -100,8 +114,15 @@ function Get-WindowsNotices([string]$Repository, [string]$Target, [string]$Notic
# conservatively. Tests/examples/benches are excluded, never all font files by glob.
if ($file.Extension -eq '.rs' -and $relative -notmatch '^(tests|examples|benches)/') {
$text = [IO.File]::ReadAllText($file.FullName)
# Full-line comments, including rustdoc examples, are not compiled
# font embeds. Keep real code and feature-gated macros conservative.
$text = [regex]::Replace($text, '(?m)^[\t ]*//[^\r\n]*', '')
foreach ($match in [regex]::Matches($text, 'include_bytes!\s*\(\s*"([^"]+\.(?:ttf|otf|woff2?))"')) {
$font = Under-Root $root ([IO.Path]::GetFullPath((Join-Path $file.DirectoryName $match.Groups[1].Value)).Substring($root.Length + 1))
$fontPath = [IO.Path]::GetFullPath((Join-Path $file.DirectoryName $match.Groups[1].Value))
$fontRoot = [IO.Path]::GetFullPath($root).TrimEnd('\', '/') + [IO.Path]::DirectorySeparatorChar
$fontComparison = if ([IO.Path]::DirectorySeparatorChar -eq '/') { [StringComparison]::Ordinal } else { [StringComparison]::OrdinalIgnoreCase }
if (-not $fontPath.StartsWith($fontRoot, $fontComparison)) { throw "Embedded font path escapes crate source root: $key/$relative" }
$font = Under-Root $root $fontPath.Substring($fontRoot.Length)
[void]$fontReferences.Add($font.Substring($root.Length + 1).Replace('\', '/'))
}
}
@@ -156,5 +177,10 @@ function Get-WindowsNotices([string]$Repository, [string]$Target, [string]$Notic
}
}
if ($failures.Count -gt 0) { throw ("Notice preflight unresolved:`n - " + ($failures -join "`n - ")) }
return [pscustomobject]@{ Inputs = $inputs; Hashes = $hashes; Inventory = [ordered]@{ format_version = 1; target = $Target; cargo_lock_sha256 = $lockHash; scope = 'Target-filtered locked offline metadata; transitive normal/build edges from oa-desktop and oa-server. Workspace feature unification may include a conservative superset. No binary/source equivalence or universal license audit claimed.'; packages = @($records.ToArray()); rust = [ordered]@{ version = $rustVersion; notices = $rustNotices; target_libraries_sha256 = $rustLibraries }; runtime = $runtime } }
return [pscustomobject]@{ Inputs = $inputs; Hashes = $hashes; Inventory = [ordered]@{ format_version = 1; target = $Target; root_packages = @($RootPackages | Sort-Object -Unique); cargo_lock_sha256 = $lockHash; workspace_build_inputs_sha256 = $workspaceInputs; scope = 'Target-filtered locked offline metadata; transitive normal/build edges from the stated root packages. Workspace feature unification may include a conservative superset. No binary/source equivalence or universal license audit claimed.'; packages = @($records.ToArray()); rust = [ordered]@{ version = $rustVersion; notices = $rustNotices; target_libraries_sha256 = $rustLibraries }; runtime = $runtime } }
}
# Compatibility entry point for the reviewed Windows pipeline and its callers.
function Get-WindowsNotices([string]$Repository, [string]$Target, [string]$NoticeManifest, [string]$LlvmMinGWRoot) {
Get-ArcadiaNotices $Repository $Target $NoticeManifest $LlvmMinGWRoot
}
@@ -0,0 +1,31 @@
Cantarell-Regular.ttf embedded by sctk-adwaita 0.10.1
Asset: src/title/Cantarell-Regular.ttf, 31236 bytes
SHA-256: d15f25bcdaba2e25f54c32d4e29e68fedeb1b7a1d8fbc83f7c4475916e93b55f
The cached crate, extracted font, and font fetched from the crate's recorded
upstream commit have matching font bytes:
https://raw.githubusercontent.com/PolyMeilex/sctk-adwaita/d5f343c45c04d4a026db57f76760819b7325db00/src/title/Cantarell-Regular.ttf
The exact font's Mac and Windows name tables contain the following records
(line endings normalized for this readable extraction):
0: Copyright (c) 2009-2011, Understanding Limited (dave@understandinglimited.com),
Copyright (c) 2010-2011, Jakub Steiner (jimmac@gmail.com).
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
5: Version 0.05
9: Dave Crossland
13: This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
14: http://scripts.sil.org/OFL
The accompanying GNOME-Cantarell-61362600e104-COPYING is the unmodified
historical GNOME Cantarell 0.0.5 license at peeled commit
61362600e104b96c88a2fdc068e1e2c68aef4f16. Its copyright holders/years and OFL 1.1
declaration match this font's embedded records. This establishes attribution
for these exact font bytes, not a claim that sctk-adwaita rebuilt the font
from that GNOME release or that arbitrary Cantarell versions are identical.
+93 -93
View File
@@ -1,93 +1,93 @@
Digitized data copyright (c) 2012-2015, The Mozilla Foundation and Telefonica S.A.
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Digitized data copyright (c) 2012-2015, The Mozilla Foundation and Telefonica S.A.
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
@@ -1,6 +1,6 @@
Extracted from the exact FiraSans-Regular.ttf name table (asset SHA-256 3dabf3d48bf4599f95cffd92f99ea426a014d5311f52a5eb5ec3af265e97cd97).
0: Digitized data copyright 2012-2016, The Mozilla Foundation and Telefonica S.A.
5: Version 4.203
13: Licensed under the Open Font License, version 1.1 or later
14: http://scripts.sil.org/OFL
0: Digitized data copyright 2012-2016, The Mozilla Foundation and Telefonica S.A.
5: Version 4.203
13: Licensed under the Open Font License, version 1.1 or later
14: http://scripts.sil.org/OFL
@@ -0,0 +1,94 @@
Copyright (c) 2009-2011, Understanding Limited (dave@understandinglimited.com),
Copyright (c) 2010-2011, Jakub Steiner (jimmac@gmail.com).
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
+19
View File
@@ -361,6 +361,25 @@
}
],
"assets": [
{
"name": "sctk-adwaita",
"version": "0.10.1",
"path": "src/title/Cantarell-Regular.ttf",
"sha256": "d15f25bcdaba2e25f54c32d4e29e68fedeb1b7a1d8fbc83f7c4475916e93b55f",
"license": "OFL-1.1",
"notices": [
{
"file": "GNOME-Cantarell-61362600e104-COPYING",
"sha256": "7bb92a19f8f5485713f6e96a8331289b3aae49e8e44e7cad58b567e03be16d76",
"url": "https://raw.githubusercontent.com/GNOME/cantarell-fonts/61362600e104b96c88a2fdc068e1e2c68aef4f16/COPYING"
},
{
"file": "Cantarell-Regular-NOTICE.txt",
"sha256": "2936eaab48b3b4ac2fa216a02f1bb0ccbbd8fe2227e9b930e526056f639def80",
"url": "https://raw.githubusercontent.com/PolyMeilex/sctk-adwaita/d5f343c45c04d4a026db57f76760819b7325db00/src/title/Cantarell-Regular.ttf"
}
]
},
{
"name": "iced_graphics",
"version": "0.14.0",