Add portable Linux packaging and verify distribution notices
This commit is contained in:
@@ -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)
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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.
|
||||
}
|
||||
@@ -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
@@ -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('\', '/')
|
||||
|
||||
@@ -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()
|
||||
@@ -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"
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user