makepad/tools/arch_usb/clone_assets.py
Admin d26814e045 platform, apps: deterministic app capture, one app registry, makepad-app-* names, and the platform fixes the Stage editor found -- the remote service gains a virtual clock (--virtual-clock; /step advances whole frames and answers after they are presented, bounded to 20 s), /settled, widget id paths in /snap (q=path:), in-process MP4 capture on that clock (/cap/start|stop|pause|resume, no overlay in the file), --window WxH@scale, a synthetic cursor drawn into frames and --seed; with those flags off nothing changes. Passes that sample another pass's target stay dirty until their source has drawn (black first frames under parallel load), a shader still compiling marks its pass dirty, the Mac text shader gets its pass's camera on the first draw, and texture readbacks go to the requester that asked (try_take_texture_readbacks_for). Splash: obj.delete removes a field, for-loop keys compare equal to their @id, a loop ending a module no longer empties the stack, and a call-scope leak is closed. CEF: one exclusive profile per process, relaunch handled without a Chrome window, JS dialogs suppressed, popups become embedded tabs, one app bundle per build. Speech: Kokoro reports word timings, whisper aligns known text (align_text) and word times are no longer a token late. libs/app_registry is the one list of apps (the WM and Builder read it, Stage finds apps by id); every app crate in apps/ is package and binary makepad-app-<id>. AGENTS.md records the current agent workflow and the rule against rm on a possibly empty variable.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 01:55:13 +02:00

190 lines
9.6 KiB
Python

"""Refresh the USB's offline assets from an installed clone, without installs."""
import base64
import hashlib
import json
from pathlib import Path
import re
import shlex
import shutil
import subprocess
import tarfile
import tempfile
HERE = Path(__file__).resolve().parent
def digest(path):
with path.open('rb') as stream:
return hashlib.file_digest(stream, 'sha256').hexdigest()
def ssh(host, *command):
if not re.fullmatch(r'(?:[a-zA-Z0-9_.-]+@)?[a-zA-Z0-9][a-zA-Z0-9_.-]*', host):
raise ValueError('Use a known SSH hostname or user@hostname for --clone')
return ['ssh', '-T', '-o', 'BatchMode=yes', '-o', 'StrictHostKeyChecking=yes',
'-o', 'UpdateHostKeys=no', '-o', 'ConnectTimeout=10', host, shlex.join(command)]
def read_database(path, repo):
records = {}
with tarfile.open(path, 'r:*') as archive:
for member in archive:
if not member.name.endswith('/desc'): continue
record = {'repo': repo}
for section in archive.extractfile(member).read().decode().strip().split('\n\n'):
lines = section.splitlines()
record[lines[0].strip('%')] = lines[1:]
records[record['NAME'][0]] = record
return records
def refresh_packages(assets, host, requested):
"""Fill gaps only from matching signed archives; retain one repo snapshot."""
mirror = assets/'package-mirror'
database = {}
for repo in ('core', 'extra'):
path = mirror/repo/'os/x86_64'/f'{repo}.db'
remote = subprocess.check_output(ssh(host, 'sha256sum', f'/var/lib/pacman/sync/{repo}.db'), text=True).split()[0]
if digest(path) != remote:
raise ValueError(f'{repo}.db differs on the clone; refusing to mix package snapshots')
database.update(read_database(path, repo))
selected = {p['NAME'][0]: p for p in json.loads((assets/'resolved-packages.json').read_text())}
for name, package in selected.items():
if any(package.get(key) != database[name].get(key) for key in ('VERSION', 'FILENAME', 'SHA256SUM')):
raise ValueError(f'Cached metadata differs from its repository snapshot: {name}')
original_names = set(selected)
queue = list(requested)
while queue:
name = queue.pop(0)
if name in selected: continue
package = database[name]
selected[name] = package
for expression in package.get('DEPENDS', []):
dependency = re.split('[<>=]', expression)[0]
if dependency in selected or any(
dependency == re.split('[<>=]', provided)[0]
for item in selected.values() for provided in item.get('PROVIDES', [])):
continue
if dependency in database:
queue.append(dependency)
else:
providers = [n for n, item in database.items() if any(
dependency == re.split('[<>=]', value)[0] for value in item.get('PROVIDES', []))]
if len(providers) != 1:
raise ValueError(f'Choose a provider for {name}: {expression}: {providers}')
queue.extend(providers)
changed = set(selected) != original_names
with tempfile.TemporaryDirectory(prefix='clone-packages-', dir=assets) as temp:
stage = Path(temp)
for name, package in sorted(selected.items()):
filename = package['FILENAME'][0]
path = mirror/package['repo']/'os/x86_64'/filename
signature = base64.b64decode(package['PGPSIG'][0])
for suffix in ('', '.sig'):
target = path.with_name(filename+suffix)
valid = target.is_file() and (
target.read_bytes() == signature if suffix else digest(target) == package['SHA256SUM'][0])
if valid: continue
# The clone retains both the original mirror and later pacman
# downloads. Neither path executes or installs the package.
candidates = [f'/var/cache/pacman/pkg/{filename}{suffix}',
f'/var/cache/makepad-mirror/{package["repo"]}/os/x86_64/{filename}{suffix}']
fetched = stage/(filename+suffix)
for remote in candidates:
with fetched.open('wb') as stream:
result = subprocess.run(ssh(host, 'cat', remote), stdout=stream, stderr=subprocess.PIPE)
if result.returncode == 0: break
else:
raise ValueError(f'Package missing from the installed clone: {filename}{suffix}')
if (fetched.read_bytes() != signature if suffix else digest(fetched) != package['SHA256SUM'][0]):
raise ValueError(f'Clone archive/signature differs from the snapshot: {filename}{suffix}')
target.parent.mkdir(parents=True, exist_ok=True)
fetched.replace(target)
changed = True
print(f'Verified {len(selected)} cached packages and their signature files.', flush=True)
if not changed: return
# Stage the complete replacement before changing the seed's manifest.
tar_path = stage/'mirror.tar'
subprocess.run(['tar', '-cf', str(tar_path), '-C', str(mirror), '.'], check=True)
chunks = []
with tar_path.open('rb') as source:
index = 0
while block := source.read(4*1024*1024):
name = f'mirror.tar.{chr(97+index//26)}{chr(97+index%26)}'
path = stage/name
with path.open('wb') as output:
remaining = 1024**3
while block:
output.write(block)
remaining -= len(block)
if not remaining: break
block = source.read(min(remaining, 4*1024*1024))
chunks.append(path)
index += 1
seed = assets/'seed'
sums = {name: value for value, name in (line.split(' ', 1) for line in (seed/'SHA256SUMS').read_text().splitlines())}
sums = {name: value for name, value in sums.items() if not name.startswith('mirror.tar.')}
for chunk in chunks:
sums[chunk.name] = digest(chunk)
for old in seed.glob('mirror.tar.*'): old.unlink()
for chunk in chunks: chunk.replace(seed/chunk.name)
(seed/'SHA256SUMS').write_text(''.join(f'{value} {name}\n' for name, value in sorted(sums.items())))
(assets/'resolved-packages.json').write_text(json.dumps([selected[n] for n in sorted(selected)], indent=2)+'\n')
print('Refreshed the offline mirror and its manifest.', flush=True)
def refresh_source(assets, host, root):
with tempfile.TemporaryDirectory(prefix='clone-source-', dir=assets) as temp:
stage = Path(temp)
payload = stage/'payload.tgz'
print(f'Exporting the working source, WM/AI Hub and Cargo cache from {host}:{root}.', flush=True)
with payload.open('wb') as output:
subprocess.run(ssh(host, 'python3', '-', root),
input=(HERE/'clone_export.py').read_bytes(), stdout=output, check=True)
unpacked = stage/'unpacked'
unpacked.mkdir()
with tarfile.open(payload, 'r:gz') as archive:
archive.extractall(unpacked, filter='data')
# The delivered source must also retain this regeneration entry point,
# even when the working clone predates it.
builder = unpacked/'source/tools/arch_usb'
builder.mkdir(parents=True, exist_ok=True)
for path in HERE.iterdir():
if path.is_file() and path.suffix in ('.py', '.sh', '.m', '.env', '.rules', '.service', '.txt'):
shutil.copy2(path, builder/path.name)
manifest = json.loads((unpacked/'manifest.json').read_text())
manifest['host'] = host
for name, expected in manifest['binary_sha256'].items():
if name not in ('makepad-app-wm', 'makepad-app-ai-hub') or digest(unpacked/'binaries'/name) != expected:
raise ValueError(f'Exported service binary hash mismatch: {name}')
source = stage/'prepared'
source.mkdir()
with tarfile.open(source/'makepad-source.tgz', 'w:gz', compresslevel=3) as archive:
archive.add(unpacked/'source', arcname='.')
with tarfile.open(source/'cargo-cache.tar', 'w') as archive:
archive.add(unpacked/'cargo', arcname='.')
shutil.copytree(unpacked/'binaries', source/'binaries')
shutil.copytree(unpacked/'config', source/'config')
for path in (source/'config').glob('*.sh'):
subprocess.run(['/bin/bash', '-n', str(path)], check=True)
revision = f'Installed clone: {host}:{root}\nExported: {manifest["exported_at"]}\nFiles: {manifest["source_files"]}\n'
(source/'source-revision.txt').write_text(revision)
manifest['files'] = {str(path.relative_to(source)): digest(path) for path in source.rglob('*') if path.is_file()}
(source/'manifest.json').write_text(json.dumps(manifest, indent=2)+'\n')
target = assets/'clone'
if target.exists():
if not (target/'manifest.json').is_file():
raise ValueError(f'Refusing to replace an unrecognized clone cache: {target}')
shutil.rmtree(target)
source.replace(target)
print('Installed clone snapshot cached and verified.', flush=True)
def verify_source(assets):
source = assets/'clone'
manifest = json.loads((source/'manifest.json').read_text())
for name, expected in manifest['files'].items():
path = source/name
if not path.resolve().is_relative_to(source.resolve()) or digest(path) != expected:
raise ValueError(f'Cached clone changed: {name}')
return source, manifest