# -*- Mode: Python -*- vi:si:et:sw=4:sts=4:ts=4:syntax=python from custom import GStreamer from cerbero.errors import FatalError from cerbero.utils import messages as m from cerbero.utils import shell, determine_num_cargo_jobs, determine_total_ram from pathlib import Path import re import shutil import tempfile class Recipe(recipe.Recipe): name = 'gst-plugins-rs' version = '0.12.8' stype = SourceType.GIT remotes = {'origin': 'https://gitlab.freedesktop.org/gstreamer/%(name)s.git'} if GStreamer.tagged_for_release: commit = f'gstreamer-{GStreamer.version}' else: commit = 'origin/0.12' # Each plugin uses one or more of these licenses. licenses = [{ License.Apachev2: ['LICENSE-APACHE'], License.MIT: ['LICENSE-MIT'], License.LGPLv2_1Plus: None, }] btype = BuildType.CARGO_C cargo_packages = [ 'audiofx', 'aws', 'cdg', 'claxon', 'closedcaption', 'dav1d', 'fallbackswitch', 'ffv1', 'fmp4', 'gif', 'hlssink3', 'hsv', 'inter', 'json', 'livesync', 'lewton', 'mp4', 'ndi', 'onvif', 'rav1e', 'regex', 'reqwest', 'raptorq', 'png', 'rtp', 'rtsp', 'textahead', 'textwrap', 'threadshare', 'togglerecord', 'tracers', 'uriplaylistbin', 'videofx', 'webrtc', 'webrtchttp', ] # If the system has less than 8GB ram or less than 4 cores, # block parallelism at both recipe and architectural level. if determine_num_cargo_jobs() == 1: allow_parallel_build = False allow_universal_parallel_build = False elif determine_total_ram() <= (32 << 30): # gst-plugins-rs peaks at ~17GB per architectural build. # Let's assume that > 32GB is enough, but that can be upped # afterwards. Currently a 32GB box freezes when trying the # universal build, as soon as it reaches the gstrswebrtc and # gstaws crates. allow_universal_parallel_build = False # Needed for openssl use_system_libs = True deps = ['gstreamer-1.0', 'gst-plugins-base-1.0', 'pango', 'cairo', 'gst-plugins-bad-1.0', 'dav1d'] def enable_plugin(self, name, category): if self.library_type in (LibraryType.SHARED, LibraryType.BOTH): attr = f'files_plugins_{category}' if not hasattr(self, attr): setattr(self, attr, []) self.update_categories() f = getattr(self, attr) f += [f'%(libdir)s/gstreamer-1.0/libgst{name}%(mext)s'] if self.library_type in (LibraryType.STATIC, LibraryType.BOTH): attr = f'files_plugins_{category}_devel' if not hasattr(self, attr): setattr(self, attr, []) d = getattr(self, attr) d += [ f'%(libdir)s/gstreamer-1.0/libgst{name}.a', f'%(libdir)s/gstreamer-1.0/libgst{name}.la', ] def prepare(self): if self.config.target_platform in (Platform.IOS, Platform.ANDROID): self.library_type = LibraryType.STATIC else: self.library_type = LibraryType.BOTH if self.config.target_platform != Platform.LINUX or self.config.cross_compiling(): self.deps.append('openssl') plugin_files = { 'core': ['fallbackswitch', 'livesync', 'rsinter', 'rstracers', 'threadshare', 'togglerecord'], 'net': ['aws', 'hlssink3', 'ndi', 'rsonvif', 'raptorq', 'reqwest', 'rsrtp', 'rsrtsp', 'webrtchttp', 'rswebrtc'], 'effects': ['rsaudiofx', 'rsvideofx'], 'codecs': ['cdg', 'claxon', 'dav1d', 'rsclosedcaption', 'ffv1', 'fmp4', 'mp4', 'gif', 'hsv', 'lewton', 'rav1e', 'json', 'rspng', 'regex', 'textwrap', 'textahead'], 'playback': ['uriplaylistbin'], } for category, names in plugin_files.items(): for name in names: self.enable_plugin(name, category) self.cargo_packages = [f'gst-plugin-{pkg}' for pkg in self.cargo_packages] # Build with Cerbero's latest glib version as minimum version self.cargo_features += ['glib/v2_74', 'gio/v2_74'] # Enable assembly optimizations via nasm self.cargo_features.append('gst-plugin-rav1e/asm') # Build with the current GStreamer version as minimum version components = ('', '-app', '-audio', '-base', '-check', '-net', '-pbutils', '-plugin-tracers', '-rtp', '-sdp', '-utils', '-video', '-webrtc') for each in components: self.cargo_features.append(f'gst{each}/v1_22') self.cargo_features.append('gst-plugin-webrtc/v1_22') async def configure(self): # Check that the Cargo.toml version matches the recipe version toml_version = self.get_cargo_toml_version() if toml_version != self.version: msg = f'{self.name} version {self.version} doesn\'t match Cargo.toml version {toml_version}' if GStreamer.tagged_for_release: raise FatalError(msg) else: m.warning(msg) await super().configure() def post_install(self): # Cargo-C currently can't install pc files into custom dirs, so we need # to move these plugin pkgconfig files to the right place. for f in self.files_list_by_category(self.DEVEL_CAT): if not f.endswith('.pc') or not 'gstreamer-1.0' in f: continue name = os.path.basename(f) src = os.path.join(self.config.libdir, 'pkgconfig', name) dst = os.path.join(self.config.prefix, f) if os.path.exists(src): shutil.copy(src, dst) # Cargo-C names MinGW DLLs as foo.dll instead of libfoo.dll # https://github.com/lu-zero/cargo-c/issues/280 # It also follows MSVC convention for static/import libraries if self.using_msvc(): for f in self.files_list_by_category(self.DEVEL_CAT): if not f.endswith('.a'): continue dst = Path(self.config.prefix) / f src = dst.with_name(f'{dst.stem[3:]}.lib') if src.exists(): shutil.copy(src, dst) else: for f in self.dist_files_list(): if not f.endswith('.dll'): continue name = os.path.basename(f) d = os.path.dirname(f) src = os.path.join(self.config.prefix, d, f'{name[3:]}') dst = os.path.join(self.config.prefix, f) if os.path.exists(src): shutil.copy(src, dst) # if there's a .dll.a it also needs renaming if os.path.exists(src + '.a'): shutil.copy(src + '.a', dst + '.a') libraries = [f for f in self.devel_files_list() if f.endswith('.a')] for f in libraries: if self.config.target_platform in (Platform.IOS, Platform.DARWIN): # Apple wants you to do Single-Object Prelink source = Path(self.config.prefix) / f # Only global symbols # Only symbol names # Use portable output format # Skip undefined symbols # Write pathname of the object file manifest = shell.check_output( [self.get_llvm_tool("llvm-nm"), "-gjPUA", source.absolute()], env=self.env, ) # Now we need to match the symbols to the pattern # Here's the catch: Apple strip is silly enough to be unable to # -undefined suppress a .o because of the -two_level_namespace being # the default post-10.1. So we need to determine which objects have # matching symbols. The rest can be safely stripped. # The symbol listing format is as follows: #  ./libgstrswebrtc.a[gstrswebrtc-3a8116aacab254c2.2u9b7sba8k2fvc9v.rcgu.o]: _gst_plugin_rswebrtc_get_desc T 500 0 # Field 1 has the object name between brackets. # Field 2 is the symbol name. symbol_pattern = re.compile('_gst_plugin_*') with tempfile.TemporaryDirectory(prefix='cerbero', dir=self.config.home_dir) as tmp: # List those symbols that will be kept symbols_to_keep = set() for line in manifest.splitlines(): data = line.split(sep=" ") symbol = data[1] if symbol_pattern.match(symbol): symbols_to_keep.add(symbol) module = ( Path(tmp) / source.name).with_suffix('.symbols') with module.open('w', encoding='utf-8') as f: f.write('# Stripped by Cerbero\n') for symbol in symbols_to_keep: f.write(f'{symbol}\n') m.log( f"Symbols to preserve in {source.absolute()}:", self.logfile ) for symbol in symbols_to_keep: m.log(f"\t{symbol}", self.logfile) # Unpack archive m.log(f"Unpacking {source.absolute()} with ar", self.logfile) shell.new_call( [shutil.which('ar'), 'xv', source.absolute()], cmd_dir=tmp, logfile=self.logfile) # Now everything is flat in the pwd m.log("Performing Single-Object Prelinking", self.logfile) prelinked_obj = ( Path(tmp) / source.name).with_suffix('.prelinked.o') ld = shutil.which("ld") if ld is None: raise FatalError(f'ld not found') # DO NOT split this into an array unless # you wrap this into a 'sh -c' call. # It needs the glob to be parsed by the shell! shell.new_call( ' '.join([ ld, "-r", "-exported_symbols_list", str(module.absolute()), "-o", str(prelinked_obj.absolute()), "*.o", ]), cmd_dir=tmp, logfile=self.logfile, ) # With the stripping done, all files now need to be rearchived dest = Path(tmp) / source.name m.log(f"Repacking library to {dest.absolute()}", self.logfile) libtool = shutil.which("libtool") if libtool is None: raise FatalError(f'libtool not found') shell.new_call([ libtool, "-static", prelinked_obj.absolute(), "-o", dest.absolute(), ], cmd_dir=tmp, logfile=self.logfile, ) # And now we paper over os.replace(dest.absolute(), source.absolute()) elif self.config.target_platform in (Platform.LINUX, Platform.ANDROID): # This is a very similar approach, however Clang # will itself do a really bad job if one supplies # a LD version script -- that'll suppress 99% of # the things needed for a working .o file. # The result, just like using # `strip --wildcard --keep-symbol=gst_plugin_*`, # is a .o that has all the symbols you want, but # placeholders/duds/broken references for all unfortunate # .rodata symbols referenced in the exported functions. # # See https://maskray.me/blog/2022-11-21-relocatable-linking source = Path(self.config.prefix) / f with tempfile.TemporaryDirectory(prefix='cerbero', dir=self.config.home_dir) as tmp: # Unpack archive m.log(f"Unpacking {source.absolute()} with ar", self.logfile) if self.config.target_platform == Platform.ANDROID: ar = shutil.which('llvm-ar', path=self.config.env['ANDROID_NDK_TOOLCHAIN_BIN']) else: ar = shutil.which('llvm-ar') if not ar: ar = shutil.which('ar') if ar is None: raise FatalError('ar not found') shell.new_call( [ar, 'xv', source.absolute()], cmd_dir=tmp, logfile=self.logfile) # Now everything is flat in the pwd m.log("Performing Single-Object Prelinking", self.logfile) prelinked_obj = ( Path(tmp) / source.name).with_suffix('.prelinked.o') if self.config.target_platform == Platform.ANDROID: ld = shutil.which("ld.lld", path=self.config.env['ANDROID_NDK_TOOLCHAIN_BIN']) else: ld = shutil.which('ld') if ld is None: raise FatalError('ld not found') # DO NOT split this into an array unless # you wrap this into a 'sh -c' call. # It needs the glob to be parsed by the shell! shell.new_call( ' '.join([ ld, "--relocatable", "--export-dynamic-symbol=gst_plugin_*", "-o", str(prelinked_obj.absolute()), "*.o", ]), cmd_dir=tmp, logfile=self.logfile, ) # WE ARE NOT DONE! ld.lld merged all the files, # stripping those not referenced in the dynamic symbol # glob, but we still need to hide all the Rust cruft. if self.config.target_platform == Platform.ANDROID: objcopy = shutil.which("llvm-objcopy", path=self.config.env['ANDROID_NDK_TOOLCHAIN_BIN']) else: objcopy = shutil.which("llvm-objcopy") if objcopy is None: objcopy = shutil.which("objcopy") if objcopy is None: raise FatalError('objcopy not found') shell.new_call( [ objcopy, "--wildcard", "--keep-global-symbol=gst_plugin_*", prelinked_obj.absolute(), ], cmd_dir=tmp, logfile=self.logfile, ) # With the stripping (really) done, all files now need to be rearchived dest = Path(tmp) / source.name m.log(f"Repacking library to {dest.absolute()}", self.logfile) shell.new_call( [ ar, 'rs', dest.absolute(), prelinked_obj.absolute() ], cmd_dir=tmp, logfile=self.logfile ) # And now we paper over os.replace(dest.absolute(), source.absolute()) super().post_install()