Fiw-Gentoo-Dots/lib/launcher.py

93 lines
3.6 KiB
Python

#!/usr/bin/env python3
"""Run the matching bundled TUI, with a Go source fallback."""
import gzip
import hashlib
import io
import json
import os
import platform
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path
REPO = Path(__file__).resolve().parents[1]
def source_hash(repo=REPO):
paths = [repo / name for name in ('go.mod', 'go.sum', 'VERSION')]
for directory in ('cmd', 'internal'):
paths += [path for path in (repo / directory).rglob('*.go') if not path.name.endswith('_test.go')]
value = hashlib.sha256()
for path in sorted(paths):
value.update(path.relative_to(repo).as_posix().encode() + b'\0' + path.read_bytes() + b'\0')
return value.hexdigest()
def digest(data):
return hashlib.sha256(data).hexdigest()
def bundled(repo, cache):
if platform.system() != 'Linux' or platform.machine() not in ('x86_64', 'amd64'):
raise RuntimeError('The bundled TUI supports Linux amd64.')
root = repo / 'assets/bin'
metadata = json.loads((root / 'tui.json').read_text())
if metadata['source_sha256'] != source_hash(repo):
raise RuntimeError('The bundled TUI does not match the frontend source in this checkout.')
binary = metadata['binaries']['linux-amd64']
path = root / binary['file']
if path.parent.resolve() != root.resolve():
raise RuntimeError('Invalid bundled TUI path.')
if cache.is_file() and not cache.is_symlink() and digest(cache.read_bytes()) == binary['sha256']:
cache.chmod(0o755)
return cache
compressed = path.read_bytes()
if digest(compressed) != binary['gzip_sha256']:
raise RuntimeError('Bundled TUI archive checksum mismatch.')
size = binary['size']
if not isinstance(size, int) or not 0 < size <= 64 * 1024 * 1024:
raise RuntimeError('Invalid bundled TUI size.')
with gzip.GzipFile(fileobj=io.BytesIO(compressed)) as stream:
data = stream.read(size + 1)
if len(data) != size or digest(data) != binary['sha256']:
raise RuntimeError('Bundled TUI binary checksum mismatch.')
cache.parent.mkdir(parents=True, exist_ok=True)
with tempfile.NamedTemporaryFile(dir=cache.parent, prefix='.fiw-dots-', delete=False) as file:
temporary = Path(file.name)
file.write(data)
try:
temporary.chmod(0o755)
temporary.replace(cache)
finally:
temporary.unlink(missing_ok=True)
return cache
def launch(repo=REPO):
os.chdir(repo)
cache = repo / 'build/fiw-dots'
if os.environ.get('FIW_DOTS_SOURCE') != '1':
try:
return bundled(repo, cache)
except (KeyError, ValueError, OSError, RuntimeError) as error:
print('Prebuilt TUI unavailable: ' + str(error), file=sys.stderr)
if not shutil.which('go'):
raise RuntimeError('Use a tagged snapshot for its prebuilt Linux amd64 TUI, or install Go 1.24+ for source builds. Python CLI commands such as ./install --plan remain available.')
print('Building the TUI from source with Go.', file=sys.stderr)
cache.parent.mkdir(parents=True, exist_ok=True)
version = (repo / 'VERSION').read_text().strip()
env = dict(os.environ, CGO_ENABLED='0', GOTOOLCHAIN='local')
subprocess.run(['go', 'build', '-trimpath', '-buildvcs=false', '-ldflags=-s -w -X main.tuiVersion=' + version,
'-o', str(cache), './cmd/dots'], env=env, check=True)
return cache
if __name__ == '__main__':
try:
binary = launch()
os.execv(binary, [str(binary)] + sys.argv[1:])
except (ValueError, OSError, RuntimeError, subprocess.CalledProcessError) as error:
print('TUI launcher: ' + str(error), file=sys.stderr)
sys.exit(1)