Files
zoitechat/packaging/appimage/check-runtime.py
T

135 lines
5.6 KiB
Python
Executable File

#!/usr/bin/env python3
"""Exercise an extracted AppImage without borrowing the build interpreter."""
import json
import os
from pathlib import Path
import subprocess
import sys
import tempfile
PROBE = r'''
import bz2, ctypes, curses, dbm.gnu, json, lzma, pathlib, readline, sqlite3, ssl, sys, tkinter, zlib
import cffi, _cffi_backend, pycparser
root = pathlib.Path(sys.argv[1]).resolve()
assert pathlib.Path(sys.base_prefix).resolve() == root / 'usr', sys.base_prefix
for module in (cffi, _cffi_backend, pycparser, ssl, sqlite3):
assert pathlib.Path(module.__file__).resolve().is_relative_to(root), module.__file__
assert cffi.FFI().sizeof('int') > 0
assert tkinter.Tcl().eval('info patchlevel').startswith('8.6.')
assert ssl.create_default_context().get_ca_certs(), 'No trusted CA certificates'
gio = ctypes.CDLL('libgio-2.0.so.0')
gio.g_tls_backend_get_default.restype = ctypes.c_void_p
gio.g_tls_backend_supports_tls.argtypes = [ctypes.c_void_p]
assert gio.g_tls_backend_supports_tls(gio.g_tls_backend_get_default()), 'GIO TLS backend unavailable'
print(json.dumps({'python': sys.version, 'openssl': ssl.OPENSSL_VERSION,
'cffi': cffi.__version__, 'pycparser': pycparser.__version__}))
'''
PYTHON_PLUGIN = r'''
import os
from pathlib import Path
import sys
import zoitechat, hexchat, xchat
__module_name__ = 'AppImage Python check'
__module_version__ = '1'
root = Path(os.environ['ZOITECHAT_SMOKE'])
(root / 'python.ok').write_text(sys.version)
def finish(userdata):
if all((root / (name + '.ok')).is_file() for name in ('python', 'perl', 'lua')):
# Return from Python before shutdown unloads the native plugin.
zoitechat.command('TIMER 0.1 KILLALL')
return False
return True
zoitechat.hook_timer(100, finish)
'''
PERL_PLUGIN = r'''
use strict;
use warnings;
use File::Spec;
use Encode;
ZoiteChat::register('AppImage Perl check', '1', 'Runtime check');
open my $marker, '>', File::Spec->catfile($ENV{ZOITECHAT_SMOKE}, 'perl.ok') or die $!;
print {$marker} $^V;
close $marker or die $!;
'''
LUA_PLUGIN = r'''
zoitechat.register('AppImage Lua check', '1', 'Runtime check')
local marker = assert(io.open(os.getenv('ZOITECHAT_SMOKE') .. '/lua.ok', 'w'))
marker:write(_VERSION)
marker:close()
'''
def run(command, env):
try:
result = subprocess.run(command, env=env, text=True, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, timeout=90)
except subprocess.TimeoutExpired as error:
raise RuntimeError(f"{command[0]} timed out:\n{error.stdout}\n{error.stderr}") from error
if result.returncode:
raise RuntimeError(f"{command[0]} failed:\n{result.stdout}\n{result.stderr}")
return result.stdout.strip()
def main():
appdir = Path(sys.argv[1]).resolve()
env = os.environ.copy()
for key in ("PYTHONHOME", "PYTHONPATH", "PERL5LIB", "LD_LIBRARY_PATH",
"GIO_MODULE_DIR", "GIO_EXTRA_MODULES"):
env.pop(key, None)
env.update({
"APPDIR": str(appdir),
"PYTHONHOME": str(appdir / "usr"),
"PYTHONNOUSERSITE": "1",
"LD_LIBRARY_PATH": f"{appdir}/usr/lib:{appdir}/usr/lib/x86_64-linux-gnu",
"SSL_CERT_FILE": str(appdir / "etc/ssl/certs/ca-certificates.crt"),
"SSL_CERT_DIR": str(appdir / "etc/ssl/certs"),
"GDK_BACKEND": "x11",
"TCL_LIBRARY": str(appdir / "usr/share/tcltk/tcl8.6"),
"TK_LIBRARY": str(appdir / "usr/share/tcltk/tk8.6"),
})
for relative in ("usr/lib/x86_64-linux-gnu/gio/modules", "usr/lib/gio/modules"):
if (appdir / relative).is_dir():
env["GIO_MODULE_DIR"] = str(appdir / relative)
env["GIO_EXTRA_MODULES"] = str(appdir / relative)
break
perl_dirs = [appdir / "usr/lib/x86_64-linux-gnu/perl-base"]
for relative in ("usr/lib/x86_64-linux-gnu/perl", "usr/lib/x86_64-linux-gnu/perl5", "usr/share/perl"):
perl_dirs.extend(sorted((appdir / relative).glob("*")))
perl_dirs.append(appdir / "usr/share/perl5")
env["PERL5LIB"] = ":".join(str(path) for path in perl_dirs if path.is_dir())
runtime = json.loads(run([str(appdir / "usr/bin/python3"), "-s", "-c", PROBE, str(appdir)], env))
expected = dict(line.split("=", 1) for line in
Path(__file__).with_name("python.env").read_text().splitlines()
if line and not line.startswith("#"))["PYTHON_VERSION"]
assert runtime["python"].split()[0] == expected, runtime
runtime["perl"] = run([str(appdir / "usr/bin/perl"), "-MFile::Spec", "-MEncode", "-MPOSIX",
"-MJSON::PP", "-e", "print $^V"], env)
with tempfile.TemporaryDirectory(prefix="zoitechat-smoke-") as temporary:
config = Path(temporary)
(config / "zoitechat.conf").write_text("gui_slist_skip = 1\n")
autoload = config / "addons"
autoload.mkdir()
for filename, content in (("smoke.py", PYTHON_PLUGIN), ("smoke.pl", PERL_PLUGIN), ("smoke.lua", LUA_PLUGIN)):
(autoload / filename).write_text(content)
env["HOME"] = temporary
env["XDG_CONFIG_HOME"] = temporary
env["ZOITECHAT_SMOKE"] = temporary
output = run(["xvfb-run", "-a", "dbus-run-session", "--", str(appdir / "AppRun"),
"--run-as-root", "--no-auto", "--cfgdir", temporary], env)
for name in ("python", "perl", "lua"):
assert (config / f"{name}.ok").is_file(), f"{name} plugin failed:\n{output}"
runtime["lua"] = (config / "lua.ok").read_text()
(appdir.parent / "runtime.json").write_text(json.dumps(runtime, indent=2) + "\n")
print(json.dumps(runtime, indent=2))
if __name__ == "__main__":
main()