Files
Cuteminded--jellyframe/tools/bundle-linux-libraries.py

125 lines
4.5 KiB
Python

#!/usr/bin/env python3
"""Bundle Ubuntu decoder libraries without replacing the host graphics drivers."""
import argparse
import json
import os
from pathlib import Path
import re
import shutil
import subprocess
HOST_LIBRARIES = re.compile(
r"^(?:lib(?:c|m|dl|pthread|rt|resolv|util|anl)\.so\."
r"|libnss_|ld-linux|lib(?:stdc\+\+|gcc_s)\.so\."
r"|lib(?:X(?!present\.so\.)|xcb|xkbcommon|wayland|decor)"
r"|lib(?:glib|gobject|gio|gmodule|gthread)-2\.0\.so\."
r"|libgdk_pixbuf-2\.0\.so\."
r"|lib(?:gtk|gdk)-[234]\.so\."
r"|lib(?:mount|blkid|uuid|smartcols)\.so\."
r"|lib(?:jpeg|tiff)\.so\."
r"|lib(?:GL|EGL|GLES|OpenGL|vulkan|drm|gbm))"
)
def run(*arguments, cwd=None):
environment = dict(os.environ, LC_ALL="C")
environment.pop("LD_LIBRARY_PATH", None)
environment.pop("LD_PRELOAD", None)
return subprocess.check_output(arguments, text=True, cwd=cwd, env=environment)
def parse_dependencies(output):
if "not found" in output:
raise RuntimeError("Unresolved shared libraries:\n" + output)
libraries = {}
for line in output.splitlines():
match = re.match(r"\s*(\S+) => (/\S+) \(", line)
if match:
libraries[match[1]] = Path(match[2])
return libraries
def dependencies(binary):
return parse_dependencies(run("ldd", str(binary)))
def package_owner(library):
resolved = library.resolve()
candidates = [str(resolved)]
if str(resolved).startswith("/usr/lib/"):
candidates.append(str(resolved)[4:])
for candidate in candidates:
try:
result = run("dpkg-query", "-S", candidate)
return result.splitlines()[0].rsplit(": ", 1)[0]
except subprocess.CalledProcessError:
continue
raise RuntimeError("No Ubuntu package owns " + str(library))
def package_metadata(package):
fields = run(
"dpkg-query", "-W", "-f=${binary:Package}\t${Version}\t${source:Package}\t${source:Version}", package
).strip().split("\t")
return dict(zip(("package", "version", "source", "source_version"), fields))
def copy_notices(packages, output):
notices = output / "licenses" / "linux-libraries"
notices.mkdir(parents=True)
for package in packages:
name = package.split(":", 1)[0]
copyright_file = Path("/usr/share/doc") / name / "copyright"
shutil.copy2(copyright_file, notices / (name + ".txt"))
def verify_bundle(plugin, library_directory, names):
resolved = dependencies(plugin)
for name in names:
if name not in resolved or resolved[name].resolve().parent != library_directory.resolve():
raise RuntimeError("The decoder does not load its bundled copy of " + name)
def bundle(input_directory, output_directory, arch):
plugin_name = "jellyframe_mpv.linux." + arch + ".so"
libraries = dependencies(input_directory / plugin_name)
bundled = {name: path for name, path in libraries.items() if not HOST_LIBRARIES.match(name)}
if "libmpv.so.2" not in bundled:
raise RuntimeError("The decoder must link to libmpv.so.2.")
packages = {}
manifest = []
for name, path in sorted(bundled.items()):
owner = package_owner(path)
if owner not in packages:
packages[owner] = package_metadata(owner)
manifest.append(dict(library=name, **packages[owner]))
shutil.copytree(input_directory, output_directory)
library_directory = output_directory / "lib"
library_directory.mkdir()
for name, path in bundled.items():
destination = library_directory / name
shutil.copy2(path, destination)
run("patchelf", "--force-rpath", "--set-rpath", "$ORIGIN", str(destination))
plugin = output_directory / plugin_name
run("patchelf", "--force-rpath", "--set-rpath", "$ORIGIN/lib", str(plugin))
verify_bundle(plugin, library_directory, bundled)
copy_notices(packages, output_directory)
manifest_text = json.dumps(manifest, indent=2) + "\n"
(output_directory / "bundled-libraries.json").write_text(manifest_text)
print("Bundled %d shared libraries from %d packages." % (len(bundled), len(packages)))
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--input-directory", type=Path, required=True)
parser.add_argument("--output-directory", type=Path, required=True)
parser.add_argument("--arch", choices=("x86_64", "arm64"), required=True)
args = parser.parse_args()
bundle(args.input_directory.resolve(), args.output_directory.resolve(), args.arch)
if __name__ == "__main__":
main()