mirror of
https://github.com/saphid/frame-control.git
synced 2026-10-25 12:01:48 +01:00
#38 built and bundled the native host into the Windows and both Linux installers (adding a Linux ARM64 installer). Only the Windows installer gets it now; the Linux and macOS installer jobs are as on main. x264 is GPL and Windows encodes with Media Foundation, so the shipped bundle leaves it out. desktop/build.py --with-x264 adds it for the native CI jobs' headless protocol tests, which no longer upload their bundles. The release job tests the packaged app's own bundle with its bundled Python and checks x264 isn't in it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
127 lines
6.0 KiB
Python
127 lines
6.0 KiB
Python
#!/usr/bin/env python3
|
|
"""Build our native PC adapter and bundle its ordinary shared libraries.
|
|
|
|
Run on the target architecture after installing GStreamer development packages.
|
|
No GStreamer executable or desktop streaming application is shipped or invoked.
|
|
Linux libc, display/GPU drivers and the portal service remain platform pieces.
|
|
|
|
x264 is GPL, so the bundle the Windows installer ships leaves it out: Windows
|
|
encodes with Media Foundation. --with-x264 adds it for CI's headless protocol
|
|
tests, which have no GPU or desktop to encode with.
|
|
"""
|
|
import argparse
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
|
|
ROOT = Path(__file__).resolve().parent
|
|
|
|
|
|
def output(*args):
|
|
return subprocess.check_output(args, text=True).strip()
|
|
|
|
|
|
def build(destination, with_x264=False):
|
|
win = sys.platform == 'win32'
|
|
destination.mkdir(parents=True, exist_ok=True)
|
|
modules = ['gstreamer-1.0', 'gstreamer-app-1.0', 'gstreamer-video-1.0']
|
|
if not win:
|
|
modules += ['gio-unix-2.0']
|
|
import shlex
|
|
flags = shlex.split(output('pkg-config', '--cflags', '--libs', *modules))
|
|
lib = destination / ('pc-host.dll' if win else 'pc-host.so')
|
|
cmd = [os.environ.get('CC', 'gcc' if win else 'cc'), '-std=c11', '-O2', '-shared', '-Wall', '-Wextra']
|
|
if not win:
|
|
cmd += ['-fPIC', '-Wl,-rpath,$ORIGIN/lib']
|
|
subprocess.run(cmd + [str(ROOT / f) for f in ('controller.c', 'capture.c', 'portal.c')] + flags + ['-o', str(lib)], check=True)
|
|
prefix = Path(output('pkg-config', '--variable=prefix', 'gstreamer-1.0'))
|
|
plugin_dir = Path(output('pkg-config', '--variable=pluginsdir', 'gstreamer-1.0'))
|
|
plugins_out, libs_out = destination / 'lib' / 'gstreamer-1.0', destination / ('bin' if win else 'lib')
|
|
plugins_out.mkdir(parents=True, exist_ok=True)
|
|
libs_out.mkdir(parents=True, exist_ok=True)
|
|
names = ['coreelements', 'videotestsrc', 'videoconvertscale', 'app', 'jpeg', 'videoparsersbad']
|
|
if with_x264:
|
|
names.append('x264')
|
|
names += ['d3d11', 'mediafoundation'] if win else ['pipewire', 'va']
|
|
pending = [lib]
|
|
for name in names:
|
|
matches = list(plugin_dir.glob('*gst' + name + ('.dll' if win else '.so')))
|
|
if not matches:
|
|
raise SystemExit('Missing GStreamer library: ' + name)
|
|
for source in matches:
|
|
target = plugins_out / source.name
|
|
shutil.copy2(source, target)
|
|
pending.append(source)
|
|
if not win:
|
|
# PipeWire dynamically loads support and protocol modules. ldd cannot
|
|
# discover those; bundle them with a private client-only config instead
|
|
# of accidentally loading a different distro's client modules/config.
|
|
libdir = plugin_dir.parent
|
|
for relative in ('pipewire-0.3/libpipewire-module-protocol-native.so',
|
|
'pipewire-0.3/libpipewire-module-client-node.so',
|
|
'pipewire-0.3/libpipewire-module-adapter.so',
|
|
'spa-0.2/support/libspa-support.so',
|
|
'spa-0.2/support/libspa-dbus.so',
|
|
'spa-0.2/videoconvert/libspa-videoconvert.so'):
|
|
source = libdir / relative
|
|
if not source.is_file():
|
|
raise SystemExit('Missing PipeWire runtime library: ' + str(source))
|
|
target = destination / 'lib' / relative
|
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
shutil.copy2(source, target)
|
|
pending.append(source)
|
|
config = destination / 'share' / 'pipewire'
|
|
config.mkdir(parents=True, exist_ok=True)
|
|
(config / 'client.conf').write_text(
|
|
'context.spa-libs = { support.* = support/libspa-support video.* = videoconvert/libspa-videoconvert }\n'
|
|
'context.modules = [\n'
|
|
' { name = libpipewire-module-protocol-native }\n'
|
|
' { name = libpipewire-module-client-node }\n'
|
|
' { name = libpipewire-module-adapter }\n'
|
|
']\n')
|
|
copied = set()
|
|
while pending:
|
|
binary = pending.pop()
|
|
if win:
|
|
imported = re.findall(r'DLL Name:\s*(\S+)', output('objdump', '-p', str(binary)))
|
|
dependencies = [prefix / 'bin' / name for name in imported]
|
|
else:
|
|
dependencies = [Path(p) for p in re.findall(r'=>\s+(/\S+)', output('ldd', str(binary)))]
|
|
for dep in dependencies:
|
|
if not dep.is_file() or dep.name in copied:
|
|
continue
|
|
if not win and re.match(r'lib(c|m|dl|rt|pthread|resolv)\.so', dep.name):
|
|
continue
|
|
shutil.copy2(dep, libs_out / dep.name)
|
|
copied.add(dep.name)
|
|
pending.append(dep)
|
|
# License texts and package provenance accompany the dynamically linked
|
|
# libraries. Distribution builders keep the upstream package/source URLs.
|
|
licenses = destination / 'licenses'
|
|
licenses.mkdir(exist_ok=True)
|
|
if win:
|
|
source = prefix / 'share' / 'licenses'
|
|
if source.exists():
|
|
shutil.copytree(source, licenses, dirs_exist_ok=True)
|
|
packages = output('pacman', '-Q') if shutil.which('pacman') else 'See MSYS2 build log'
|
|
else:
|
|
packages = output('dpkg-query', '-W', '-f=${Package} ${Version}\n') if shutil.which('dpkg-query') else ''
|
|
# Debian copyright files contain licenses and source homepage details.
|
|
for path in Path('/usr/share/doc').glob('*/copyright'):
|
|
shutil.copy2(path, licenses / (path.parent.name + '.copyright'))
|
|
(destination / 'libraries.json').write_text(json.dumps(dict(gstreamer=output('pkg-config', '--modversion', 'gstreamer-1.0'),
|
|
plugins=names, libraries=sorted(copied), packages=packages), indent=2))
|
|
print(lib)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('--out', type=Path, default=ROOT / 'bundle')
|
|
parser.add_argument('--with-x264', action='store_true', help='add the GPL x264 encoder (CI tests only, never shipped)')
|
|
args = parser.parse_args()
|
|
build(args.out.resolve(), args.with_x264)
|