aboutsummaryrefslogtreecommitdiffstats
path: root/python/servo
diff options
context:
space:
mode:
Diffstat (limited to 'python/servo')
-rw-r--r--python/servo/build_commands.py107
-rw-r--r--python/servo/gstreamer.py156
-rw-r--r--python/servo/package_commands.py8
-rw-r--r--python/servo/platform/macos.py2
4 files changed, 160 insertions, 113 deletions
diff --git a/python/servo/build_commands.py b/python/servo/build_commands.py
index 93a432041df..61cede7e7b3 100644
--- a/python/servo/build_commands.py
+++ b/python/servo/build_commands.py
@@ -31,11 +31,12 @@ from mach.decorators import (
from mach.registrar import Registrar
import servo.platform
+import servo.platform.macos
import servo.util
import servo.visual_studio
from servo.command_base import BuildType, CommandBase, call, check_call
-from servo.gstreamer import windows_dlls, windows_plugins, macos_plugins
+from servo.gstreamer import windows_dlls, windows_plugins, package_gstreamer_dylibs
SUPPORTED_ASAN_TARGETS = ["aarch64-apple-darwin", "aarch64-unknown-linux-gnu",
"x86_64-apple-darwin", "x86_64-unknown-linux-gnu"]
@@ -154,8 +155,10 @@ class MachCommands(CommandBase):
assert os.path.exists(servo_bin_dir)
if self.enable_media:
- print("Packaging gstreamer dylibs")
- if not package_gstreamer_dylibs(self.cross_compile_target, built_binary):
+ library_target_directory = path.join(path.dirname(built_binary), "lib/")
+ if not package_gstreamer_dylibs(built_binary,
+ library_target_directory,
+ self.cross_compile_target):
return 1
# On the Mac, set a lovely icon. This makes it easier to pick out the Servo binary in tools
@@ -293,104 +296,6 @@ class MachCommands(CommandBase):
print(f"[Warning] Could not generate notification: {e}", file=sys.stderr)
-def otool(s):
- o = subprocess.Popen(['/usr/bin/otool', '-L', s], stdout=subprocess.PIPE)
- for line in map(lambda s: s.decode('ascii'), o.stdout):
- if line[0] == '\t':
- yield line.split(' ', 1)[0][1:]
-
-
-def install_name_tool(binary, *args):
- try:
- subprocess.check_call(['install_name_tool', *args, binary])
- except subprocess.CalledProcessError as e:
- print("install_name_tool exited with return value %d" % e.returncode)
-
-
-def change_link_name(binary, old, new):
- install_name_tool(binary, '-change', old, f"@executable_path/{new}")
-
-
-def is_system_library(lib):
- return lib.startswith("/System/Library") or lib.startswith("/usr/lib") or ".asan." in lib
-
-
-def is_relocatable_library(lib):
- return lib.startswith("@rpath/")
-
-
-def change_non_system_libraries_path(libraries, relative_path, binary):
- for lib in libraries:
- if is_system_library(lib) or is_relocatable_library(lib):
- continue
- new_path = path.join(relative_path, path.basename(lib))
- change_link_name(binary, lib, new_path)
-
-
-def resolve_rpath(lib, rpath_root):
- if not is_relocatable_library(lib):
- return lib
-
- rpaths = ['', '../', 'gstreamer-1.0/']
- for rpath in rpaths:
- full_path = rpath_root + lib.replace('@rpath/', rpath)
- if path.exists(full_path):
- return path.normpath(full_path)
-
- raise Exception("Unable to satisfy rpath dependency: " + lib)
-
-
-def copy_dependencies(binary_path, lib_path, gst_lib_dir):
- relative_path = path.relpath(lib_path, path.dirname(binary_path)) + "/"
-
- # Update binary libraries
- binary_dependencies = set(otool(binary_path))
- change_non_system_libraries_path(binary_dependencies, relative_path, binary_path)
-
- plugins = [os.path.join(gst_lib_dir, "gstreamer-1.0", plugin) for plugin in macos_plugins()]
- binary_dependencies = binary_dependencies.union(plugins)
-
- # Update dependencies libraries
- need_checked = binary_dependencies
- checked = set()
- while need_checked:
- checking = set(need_checked)
- need_checked = set()
- for f in checking:
- # No need to check these for their dylibs
- if is_system_library(f):
- continue
- full_path = resolve_rpath(f, gst_lib_dir)
- need_relinked = set(otool(full_path))
- new_path = path.join(lib_path, path.basename(full_path))
- if not path.exists(new_path):
- shutil.copyfile(full_path, new_path)
- change_non_system_libraries_path(need_relinked, relative_path, new_path)
- need_checked.update(need_relinked)
- checked.update(checking)
- need_checked.difference_update(checked)
-
-
-def package_gstreamer_dylibs(cross_compilation_target, servo_bin):
- gst_root = servo.platform.get().gstreamer_root(cross_compilation_target)
-
- # This might be None if we are cross-compiling.
- if not gst_root:
- return True
-
- lib_dir = path.join(path.dirname(servo_bin), "lib")
- if os.path.exists(lib_dir):
- shutil.rmtree(lib_dir)
- os.mkdir(lib_dir)
- try:
- copy_dependencies(servo_bin, lib_dir, path.join(gst_root, 'lib', ''))
- except Exception as e:
- print("ERROR: could not package required dylibs")
- print(e)
- return False
- return True
-
-
def copy_windows_dlls_to_build_directory(servo_binary: str, target_triple: str) -> bool:
servo_exe_dir = os.path.dirname(servo_binary)
assert os.path.exists(servo_exe_dir)
diff --git a/python/servo/gstreamer.py b/python/servo/gstreamer.py
index 3c63b7dbd5d..b7e68a0772f 100644
--- a/python/servo/gstreamer.py
+++ b/python/servo/gstreamer.py
@@ -7,8 +7,11 @@
# option. This file may not be copied, modified, or distributed
# except according to those terms.
-import os
+import os.path
+import shutil
+import subprocess
import sys
+from typing import Set
GSTREAMER_BASE_LIBS = [
# gstreamer
@@ -157,11 +160,6 @@ def windows_plugins():
return [f"{lib}.dll" for lib in libs]
-def macos_gst_root():
- return os.path.join(
- "/", "Library", "Frameworks", "GStreamer.framework", "Versions", "1.0")
-
-
def macos_plugins():
plugins = [
*GSTREAMER_PLUGIN_LIBS,
@@ -185,5 +183,151 @@ pub(crate) static GSTREAMER_PLUGINS: &[&'static str] = &[
''' % ',\n'.join(map(lambda x: '"' + x + '"', plugins)))
+def is_macos_system_library(library_path: str) -> bool:
+ """Returns true if if the given dependency line from otool refers to
+ a system library that should not be packaged."""
+ return (library_path.startswith("/System/Library")
+ or library_path.startswith("/usr/lib")
+ or ".asan." in library_path)
+
+
+def rewrite_dependencies_to_be_relative(binary: str, dependency_lines: Set[str], relative_path: str):
+ """Given a path to a binary (either an executable or a dylib), rewrite the
+ the given dependency lines to be found at the given relative path to
+ the executable in which they are used. In our case, this is typically servoshell."""
+ for dependency_line in dependency_lines:
+ if is_macos_system_library(dependency_line) or dependency_line.startswith("@rpath/"):
+ continue
+
+ new_path = os.path.join("@executable_path", relative_path, os.path.basename(dependency_line))
+ arguments = ['install_name_tool', '-change', dependency_line, new_path, binary]
+ try:
+ subprocess.check_call(arguments)
+ except subprocess.CalledProcessError as exception:
+ print(f"{arguments} install_name_tool exited with return value {exception.returncode}")
+
+
+def make_rpath_path_absolute(dylib_path_from_otool: str, rpath: str):
+ """Given a dylib dependency from otool, resolve the path into a full path if it
+ contains `@rpath`."""
+ if not dylib_path_from_otool.startswith("@rpath/"):
+ return dylib_path_from_otool
+
+ # Not every dependency is in the same directory as the binary that is references. For
+ # instance, plugins dylibs can be found in "gstreamer-1.0".
+ path_relative_to_rpath = dylib_path_from_otool.replace('@rpath/', '')
+ for relative_directory in ["", "..", "gstreamer-1.0"]:
+ full_path = os.path.join(rpath, relative_directory, path_relative_to_rpath)
+ if os.path.exists(full_path):
+ return os.path.normpath(full_path)
+
+ raise Exception("Unable to satisfy rpath dependency: " + dylib_path_from_otool)
+
+
+def find_non_system_dependencies_with_otool(binary_path: str) -> Set[str]:
+ """Given a binary path, find all dylib dependency lines that do not refer to
+ system libraries."""
+ process = subprocess.Popen(['/usr/bin/otool', '-L', binary_path], stdout=subprocess.PIPE)
+ output = set()
+
+ for line in map(lambda line: line.decode('utf8'), process.stdout):
+ if not line.startswith("\t"):
+ continue
+ dependency = line.split(' ', 1)[0][1:]
+
+ # No need to do any processing for system libraries. They should be
+ # present on all macOS systems.
+ if not is_macos_system_library(dependency):
+ output.add(dependency)
+ return output
+
+
+def package_gstreamer_dylibs(binary_path: str, library_target_directory: str, cross_compilation_target: str = None):
+ """Copy all GStreamer dependencies to the "lib" subdirectory of a built version of
+ Servo. Also update any transitive shared library paths so that they are relative to
+ this subdirectory."""
+
+ # This import only works when called from `mach`.
+ import servo.platform
+
+ gstreamer_root = servo.platform.get().gstreamer_root(cross_compilation_target)
+ gstreamer_version = servo.platform.macos.GSTREAMER_PLUGIN_VERSION
+ gstreamer_root_libs = os.path.join(gstreamer_root, "lib")
+
+ # This is the relative path from the directory we are packaging the dylibs into and
+ # the binary we are packaging them for.
+ relative_path = os.path.relpath(library_target_directory, os.path.dirname(binary_path)) + "/"
+
+ # This might be None if we are cross-compiling.
+ if not gstreamer_root:
+ return True
+
+ # Detect when the packaged library versions do not reflect our current version of GStreamer,
+ # by writing a marker file with the packaged GStreamer version into the target directory.
+ marker_file = os.path.join(library_target_directory, f".gstreamer-{gstreamer_version}")
+
+ print()
+ if os.path.exists(library_target_directory) and os.path.exists(marker_file):
+ print(" • GStreamer packaging is up-to-date")
+ return True
+
+ if os.path.exists(library_target_directory):
+ print(" • Packaged GStreamer is out of date. Rebuilding into {library_target_directory}")
+ shutil.rmtree(library_target_directory)
+ else:
+ print(f" • Packaging GStreamer into {library_target_directory}")
+
+ os.makedirs(library_target_directory, exist_ok=True)
+ try:
+ # Collect all the initial binary dependencies for Servo and the plugins that it uses,
+ # which are loaded dynmically at runtime and don't appear in `otool` output.
+ binary_dependencies = set(find_non_system_dependencies_with_otool(binary_path))
+ binary_dependencies.update(
+ [os.path.join(gstreamer_root_libs, "gstreamer-1.0", plugin)
+ for plugin in macos_plugins()]
+ )
+
+ rewrite_dependencies_to_be_relative(binary_path, binary_dependencies, relative_path)
+
+ number_copied = 0
+ pending_to_be_copied = binary_dependencies
+ already_copied = set()
+
+ while pending_to_be_copied:
+ checking = set(pending_to_be_copied)
+ pending_to_be_copied.clear()
+
+ for otool_dependency in checking:
+ already_copied.add(otool_dependency)
+
+ original_dylib_path = make_rpath_path_absolute(otool_dependency, gstreamer_root_libs)
+ transitive_dependencies = set(find_non_system_dependencies_with_otool(original_dylib_path))
+
+ # First copy the dylib into the directory where we are collecting them all for
+ # packaging, and rewrite its dependencies to be relative to the executable we
+ # are packaging them for.
+ new_dylib_path = os.path.join(library_target_directory, os.path.basename(original_dylib_path))
+ if not os.path.exists(new_dylib_path):
+ number_copied += 1
+ shutil.copyfile(original_dylib_path, new_dylib_path)
+ rewrite_dependencies_to_be_relative(new_dylib_path, transitive_dependencies, relative_path)
+
+ # Now queue up any transitive dependencies for processing in further iteration loops.
+ transitive_dependencies.difference_update(already_copied)
+ pending_to_be_copied.update(transitive_dependencies)
+
+ except Exception as exception:
+ print(f"ERROR: could not package required dylibs: {exception}")
+ raise exception
+
+ with open(marker_file, "w") as file:
+ file.write(gstreamer_version)
+
+ if number_copied:
+ print(f" • Processed {number_copied} GStreamer dylibs. ")
+ print(" This can cause the startup to be slow due to macOS security protections.")
+ return True
+
+
if __name__ == "__main__":
write_plugin_list(sys.argv[1])
diff --git a/python/servo/package_commands.py b/python/servo/package_commands.py
index a9470b7b372..284f2df86ef 100644
--- a/python/servo/package_commands.py
+++ b/python/servo/package_commands.py
@@ -22,6 +22,7 @@ import shutil
import subprocess
import sys
+import servo.gstreamer
from mach.decorators import (
CommandArgument,
CommandProvider,
@@ -38,8 +39,6 @@ from servo.command_base import (
is_macosx,
is_windows,
)
-from servo.build_commands import copy_dependencies
-from servo.gstreamer import macos_gst_root
from servo.util import delete, get_target_dir
PACKAGES = {
@@ -218,10 +217,9 @@ class PackageCommands(CommandBase):
change_prefs(dir_to_resources, "macosx")
- print("Finding dylibs and relinking")
+ print("Packaging GStreamer...")
dmg_binary = path.join(content_dir, "servo")
- dir_to_gst_lib = path.join(macos_gst_root(), 'lib', '')
- copy_dependencies(dmg_binary, lib_dir, dir_to_gst_lib)
+ servo.gstreamer.package_gstreamer_dylibs(dmg_binary, lib_dir)
print("Adding version to Credits.rtf")
version_command = [binary_path, '--version']
diff --git a/python/servo/platform/macos.py b/python/servo/platform/macos.py
index eeb2ee26c1c..de20fe35ad5 100644
--- a/python/servo/platform/macos.py
+++ b/python/servo/platform/macos.py
@@ -16,6 +16,7 @@ from .. import util
from .base import Base
URL_BASE = "https://github.com/servo/servo-build-deps/releases/download/macOS"
+GSTREAMER_PLUGIN_VERSION = "1.22.3"
GSTREAMER_URL = f"{URL_BASE}/gstreamer-1.0-1.22.3-universal.pkg"
GSTREAMER_DEVEL_URL = f"{URL_BASE}/gstreamer-1.0-devel-1.22.3-universal.pkg"
GSTREAMER_ROOT = "/Library/Frameworks/GStreamer.framework/Versions/1.0"
@@ -34,7 +35,6 @@ class MacOS(Base):
def is_gstreamer_installed(self, cross_compilation_target: Optional[str]) -> bool:
# Servo only supports the official GStreamer distribution on MacOS.
- # Make sure we use the right `pkg-config`.
return not cross_compilation_target and os.path.exists(GSTREAMER_ROOT)
def _platform_bootstrap(self, _force: bool) -> bool: