mirror of
https://github.com/Cuteminded/jellyframe.git
synced 2026-10-18 16:00:30 +02:00
125 lines
4.5 KiB
Python
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()
|