aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMartin Robinson <mrobinson@igalia.com>2024-08-04 14:00:15 +0200
committerGitHub <noreply@github.com>2024-08-04 12:00:15 +0000
commitb366a02318def70948f8ff6ed321e433b721ece4 (patch)
tree432b2621b703d8adcac554b348954a864a25af1c
parent8052027dd497241157fc74365c5c78fde028b8a0 (diff)
downloadservo-b366a02318def70948f8ff6ed321e433b721ece4.tar.gz
servo-b366a02318def70948f8ff6ed321e433b721ece4.zip
build: Speed up first run after build on macOS (#32928)
GStreamer has to process plugins each time they are added when initializing. When those files have changed, this triggers macOS security protections which can add many seconds to access time. This change eliminates that problem after the first packaging of libraries by skipping packaging if everything is up-to-date and not overwriting the dylibs everytime. In addition, it moves a lot of the code for packaging GStreamer libraries on macOS into the `gstreamer` module and adds type-safety and comments to the Python. Signed-off-by: Martin Robinson <mrobinson@igalia.com>
-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: