diff options
Diffstat (limited to 'python/servo')
-rw-r--r-- | python/servo/build_commands.py | 32 | ||||
-rw-r--r-- | python/servo/command_base.py | 127 | ||||
-rw-r--r-- | python/servo/devenv_commands.py | 9 | ||||
-rw-r--r-- | python/servo/platform/__init__.py | 26 | ||||
-rw-r--r-- | python/servo/platform/base.py | 100 | ||||
-rw-r--r-- | python/servo/platform/linux.py | 53 | ||||
-rw-r--r-- | python/servo/platform/macos.py | 86 | ||||
-rw-r--r-- | python/servo/platform/windows.py | 97 | ||||
-rw-r--r-- | python/servo/post_build_commands.py | 21 | ||||
-rw-r--r-- | python/servo/testing_commands.py | 16 | ||||
-rw-r--r-- | python/servo/util.py | 66 |
11 files changed, 400 insertions, 233 deletions
diff --git a/python/servo/build_commands.py b/python/servo/build_commands.py index c2e3459cfbd..5c016b82200 100644 --- a/python/servo/build_commands.py +++ b/python/servo/build_commands.py @@ -24,6 +24,8 @@ import zipfile from time import time import notifypy +import servo.platform +import servo.util from mach.decorators import ( CommandArgument, @@ -33,9 +35,8 @@ from mach.decorators import ( from mach.registrar import Registrar from mach_bootstrap import _get_exec_path -from servo.command_base import CommandBase, cd, call, check_call, append_to_path_env, gstreamer_root +from servo.command_base import CommandBase, cd, call, check_call from servo.gstreamer import windows_dlls, windows_plugins, macos_plugins -from servo.platform import host_triple @CommandProvider @@ -92,7 +93,7 @@ class MachCommands(CommandBase): features += media_stack has_media_stack = media_stack[0] == "media-gstreamer" - target_path = base_path = self.get_target_dir() + target_path = base_path = servo.util.get_target_dir() if android: target_path = path.join(target_path, "android") base_path = path.join(target_path, target) @@ -141,8 +142,8 @@ class MachCommands(CommandBase): build_start = time() env["CARGO_TARGET_DIR"] = target_path - host = host_triple() - target_triple = target or host_triple() + host = servo.platform.host_triple() + target_triple = target or servo.platform.host_triple() if 'apple-darwin' in host and target_triple == host: if 'CXXFLAGS' not in env: env['CXXFLAGS'] = '' @@ -191,7 +192,7 @@ class MachCommands(CommandBase): # Ensure that the NuGet ANGLE package containing libEGL is accessible # to the Rust linker. - append_to_path_env(angle_root(target_triple, env), env, "LIB") + servo.util.append_paths_to_env(env, "LIB", angle_root(target_triple, env)) # Don't want to mix non-UWP libraries with vendored UWP libraries. if "gstreamer" in env['LIB']: @@ -224,12 +225,6 @@ class MachCommands(CommandBase): print(stderr.decode(encoding)) exit(1) - # Ensure that GStreamer libraries are accessible when linking. - if 'windows' in target_triple: - gst_root = gstreamer_root(target_triple, env) - if gst_root: - append_to_path_env(os.path.join(gst_root, "lib"), env, "LIB") - if android: if "ANDROID_NDK" not in env: print("Please set the ANDROID_NDK environment variable.") @@ -511,9 +506,8 @@ class MachCommands(CommandBase): assert os.path.exists(servo_bin_dir) if has_media_stack: - gst_root = gstreamer_root(target, env) print("Packaging gstreamer dylibs") - if not package_gstreamer_dylibs(gst_root, servo_path): + if not package_gstreamer_dylibs(target, servo_path): return 1 # On Mac we use the relocatable dylibs from offical gstreamer @@ -776,7 +770,13 @@ def copy_dependencies(binary_path, lib_path, gst_root): need_checked.difference_update(checked) -def package_gstreamer_dylibs(gst_root, servo_bin): +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) @@ -791,7 +791,7 @@ def package_gstreamer_dylibs(gst_root, servo_bin): def package_gstreamer_dlls(env, servo_exe_dir, target, uwp): - gst_root = gstreamer_root(target, env) + gst_root = servo.platform.get().gstreamer_root(cross_compilation_target=target) if not gst_root: print("Could not find GStreamer installation directory.") return False diff --git a/python/servo/command_base.py b/python/servo/command_base.py index e7536ad4e30..e6209b0cf23 100644 --- a/python/servo/command_base.py +++ b/python/servo/command_base.py @@ -34,12 +34,12 @@ from subprocess import PIPE import toml import servo.platform +import servo.util as util from xml.etree.ElementTree import XML from servo.util import download_file, get_default_cache_dir from mach.decorators import CommandArgument from mach.registrar import Registrar -from servo.gstreamer import macos_gst_root BIN_SUFFIX = ".exe" if sys.platform == "win32" else "" NIGHTLY_REPOSITORY_URL = "https://servo-builds2.s3.amazonaws.com/" @@ -213,37 +213,6 @@ def is_linux(): return sys.platform.startswith('linux') -def append_to_path_env(string, env, name): - variable = "" - if name in env: - variable = six.ensure_str(env[name]) - if len(variable) > 0: - variable += os.pathsep - variable += string - env[name] = variable - - -def gstreamer_root(target, env, topdir=None): - if is_windows(): - arch = { - "x86_64": "X86_64", - "x86": "X86", - "aarch64": "ARM64", - } - gst_x64 = arch[target.split('-')[0]] - gst_default_path = path.join("C:\\gstreamer\\1.0", gst_x64) - gst_env = "GSTREAMER_1_0_ROOT_" + gst_x64 - if env.get(gst_env) is not None: - return env.get(gst_env) - elif os.path.exists(path.join(gst_default_path, "bin", "ffi-7.dll")): - return gst_default_path - elif is_linux(): - return path.join(topdir, "support", "linux", "gstreamer", "gst") - elif is_macosx(): - return macos_gst_root() - return None - - class BuildNotFound(Exception): def __init__(self, message): self.message = message @@ -343,14 +312,8 @@ class CommandBase(object): def get_top_dir(self): return self.context.topdir - def get_target_dir(self): - if "CARGO_TARGET_DIR" in os.environ: - return os.environ["CARGO_TARGET_DIR"] - else: - return path.join(self.context.topdir, "target") - def get_apk_path(self, release): - base_path = self.get_target_dir() + base_path = util.get_target_dir() base_path = path.join(base_path, "android", self.config["android"]["target"]) apk_name = "servoapp.apk" build_type = "release" if release else "debug" @@ -359,7 +322,7 @@ class CommandBase(object): def get_binary_path(self, release, dev, target=None, android=False, simpleservo=False): # TODO(autrilla): this function could still use work - it shouldn't # handle quitting, or printing. It should return the path, or an error. - base_path = self.get_target_dir() + base_path = util.get_target_dir() binary_name = "servo" + BIN_SUFFIX @@ -532,56 +495,6 @@ class CommandBase(object): return self.get_executable(destination_folder) - def needs_gstreamer_env(self, target, env, uwp=False, features=[]): - if uwp: - return False - if "media-dummy" in features: - return False - - # MacOS always needs the GStreamer environment variable, but should - # also check that the Servo-specific version is downloaded and available. - if is_macosx(): - if servo.platform.get().is_gstreamer_installed(): - return True - else: - raise Exception("Official GStreamer framework not found (we need at least 1.21)." - "Please see installation instructions in README.md") - - try: - if servo.platform.get().is_gstreamer_installed(): - return False - except Exception: - # Some systems don't have pkg-config; we can't probe in this case - # and must hope for the best - return False - effective_target = target or servo.platform.host_triple() - if "x86_64" not in effective_target or "android" in effective_target: - # We don't build gstreamer for non-x86_64 / android yet - return False - if is_linux() or is_windows(): - if path.isdir(gstreamer_root(effective_target, env, self.get_top_dir())): - return True - else: - raise Exception("Your system's gstreamer libraries are out of date \ -(we need at least 1.16). Please run ./mach bootstrap-gstreamer") - else: - raise Exception("Your system's gstreamer libraries are out of date \ -(we need at least 1.16). If you're unable to \ -install them, let us know by filing a bug!") - return False - - def set_run_env(self, android=False): - """Some commands, like test-wpt, don't use a full build env, - but may still need dynamic search paths. This command sets that up""" - if not android and self.needs_gstreamer_env(None, os.environ): - gstpath = gstreamer_root(servo.platform.host_triple(), os.environ, self.get_top_dir()) - if gstpath is None: - return - os.environ["LD_LIBRARY_PATH"] = path.join(gstpath, "lib") - os.environ["GST_PLUGIN_SYSTEM_PATH"] = path.join(gstpath, "lib", "gstreamer-1.0") - os.environ["PKG_CONFIG_PATH"] = path.join(gstpath, "lib", "pkgconfig") - os.environ["GST_PLUGIN_SCANNER"] = path.join(gstpath, "libexec", "gstreamer-1.0", "gst-plugin-scanner") - def msvc_package_dir(self, package): return path.join(self.context.sharedir, "msvc-dependencies", package, servo.platform.windows.DEPENDENCIES[package]) @@ -606,6 +519,10 @@ install them, let us know by filing a bug!") def build_env(self, hosts_file_path=None, target=None, is_build=False, test_unit=False, uwp=False, features=None): """Return an extended environment dictionary.""" env = os.environ.copy() + + if not features or "media-dummy" not in features: + servo.platform.get().set_gstreamer_environment_variables_if_necessary(env, cross_compilation_target=target) + if sys.platform == "win32" and type(env['PATH']) == six.text_type: # On win32, the virtualenv's activate_this.py script sometimes ends up # turning os.environ['PATH'] into a unicode string. This doesn't work @@ -615,7 +532,6 @@ install them, let us know by filing a bug!") # it in any case. env['PATH'] = env['PATH'].encode('ascii', 'ignore') extra_path = [] - extra_lib = [] if "msvc" in (target or servo.platform.host_triple()): extra_path += [path.join(self.msvc_package_dir("cmake"), "bin")] extra_path += [path.join(self.msvc_package_dir("llvm"), "bin")] @@ -666,19 +582,6 @@ install them, let us know by filing a bug!") # Always build harfbuzz from source env["HARFBUZZ_SYS_NO_PKG_CONFIG"] = "true" - if is_build and self.needs_gstreamer_env(target or servo.platform.host_triple(), env, uwp, features): - gst_root = gstreamer_root(target or servo.platform.host_triple(), env, self.get_top_dir()) - bin_path = path.join(gst_root, "bin") - lib_path = path.join(gst_root, "lib") - pkg_config_path = path.join(lib_path, "pkgconfig") - # we append in the reverse order so that system gstreamer libraries - # do not get precedence - extra_path = [bin_path] + extra_path - extra_lib = [lib_path] + extra_lib - append_to_path_env(pkg_config_path, env, "PKG_CONFIG_PATH") - if is_macosx(): - env["OPENSSL_INCLUDE_DIR"] = path.join(gst_root, "Headers") - if is_linux(): distrib, version, _ = distro.linux_distribution() distrib = six.ensure_str(distrib) @@ -687,17 +590,13 @@ install them, let us know by filing a bug!") env["HARFBUZZ_SYS_NO_PKG_CONFIG"] = "true" if extra_path: - append_to_path_env(os.pathsep.join(extra_path), env, "PATH") + util.append_paths_to_env(env, "PATH", extra_path) if self.config["build"]["incremental"]: env["CARGO_INCREMENTAL"] = "1" elif self.config["build"]["incremental"] is not None: env["CARGO_INCREMENTAL"] = "0" - if extra_lib: - path_var = "DYLD_LIBRARY_PATH" if sys.platform == "darwin" else "LD_LIBRARY_PATH" - append_to_path_env(os.pathsep.join(extra_lib), env, path_var) - # Paths to Android build tools: if self.config["android"]["sdk"]: env["ANDROID_SDK"] = self.config["android"]["sdk"] @@ -737,6 +636,14 @@ install them, let us know by filing a bug!") # This wrapper script is in bash and doesn't work on Windows # where we want to run doctests as part of `./mach test-unit` env['RUSTDOC'] = path.join(self.context.topdir, 'etc', 'rustdoc-with-private') + elif "msvc" in servo.platform.host_triple(): + # on MSVC, we need some DLLs in the path. They were copied + # in to the servo.exe build dir, so just point PATH to that. + util.prepend_paths_to_env(env, "PATH", path.dirname(self.get_binary_path(False, False))) + + # FIXME: https://github.com/servo/servo/issues/26192 + if test_unit and "apple-darwin" not in servo.platform.host_triple(): + env["RUST_BACKTRACE"] = "1" if self.config["build"]["rustflags"]: env['RUSTFLAGS'] = env.get('RUSTFLAGS', "") + " " + self.config["build"]["rustflags"] @@ -1067,7 +974,7 @@ install them, let us know by filing a bug!") def ensure_clobbered(self, target_dir=None): if target_dir is None: - target_dir = self.get_target_dir() + target_dir = util.get_target_dir() auto = True if os.environ.get('AUTOCLOBBER', False) else False src_clobber = os.path.join(self.context.topdir, 'CLOBBER') target_clobber = os.path.join(target_dir, 'CLOBBER') diff --git a/python/servo/devenv_commands.py b/python/servo/devenv_commands.py index f69a1c8231f..77b95ba743f 100644 --- a/python/servo/devenv_commands.py +++ b/python/servo/devenv_commands.py @@ -48,9 +48,8 @@ class MachCommands(CommandBase): self.ensure_bootstrapped(target=target) self.ensure_clobbered() - env = self.build_env() - status = self.run_cargo_build_like_command("check", params, env=env, features=features, **kwargs) + status = self.run_cargo_build_like_command("check", params, features=features, **kwargs) if status == 0: print('Finished checking, binary NOT updated. Consider ./mach build before ./mach run') @@ -142,9 +141,8 @@ class MachCommands(CommandBase): self.ensure_bootstrapped(target=target) self.ensure_clobbered() - env = self.build_env() - return self.run_cargo_build_like_command("fix", params, env=env, features=features, **kwargs) + return self.run_cargo_build_like_command("fix", params, features=features, **kwargs) @Command('cargo-clippy', description='Run "cargo clippy"', @@ -166,9 +164,8 @@ class MachCommands(CommandBase): self.ensure_bootstrapped(target=target) self.ensure_clobbered() - env = self.build_env() - return self.run_cargo_build_like_command("clippy", params, env=env, features=features, **kwargs) + return self.run_cargo_build_like_command("clippy", params, features=features, **kwargs) @Command('grep', description='`git grep` for selected directories.', diff --git a/python/servo/platform/__init__.py b/python/servo/platform/__init__.py index 84968550d00..e06bebaf863 100644 --- a/python/servo/platform/__init__.py +++ b/python/servo/platform/__init__.py @@ -9,9 +9,10 @@ import platform -from .base import Base from .windows import Windows +__platform__ = None + def host_platform(): os_type = platform.system().lower() @@ -47,18 +48,27 @@ def host_triple(): def get(): + # pylint: disable=global-statement + global __platform__ + if __platform__: + return __platform__ + # We import the concrete platforms in if-statements here, because # each one might have platform-specific imports which might not # resolve on all platforms. # TODO(mrobinson): We should do this for Windows too, once we # stop relying on platform-specific code outside of this module. # pylint: disable=import-outside-toplevel - if "windows-msvc" in host_triple(): - return Windows() - if "linux-gnu" in host_triple(): + triple = host_triple() + if "windows-msvc" in triple: + __platform__ = Windows(triple) + elif "linux-gnu" in triple: from .linux import Linux - return Linux() - if "apple-darwin" in host_triple(): + __platform__ = Linux(triple) + elif "apple-darwin" in triple: from .macos import MacOS - return MacOS() - return Base() + __platform__ = MacOS(triple) + else: + from .base import Base + __platform__ = Base(triple) + return __platform__ diff --git a/python/servo/platform/base.py b/python/servo/platform/base.py index 316c998b1db..b0ac64386bd 100644 --- a/python/servo/platform/base.py +++ b/python/servo/platform/base.py @@ -7,28 +7,104 @@ # option. This file may not be copied, modified, or distributed # except according to those terms. +import os import subprocess +from typing import Dict, Optional + +from .. import util class Base: + def __init__(self, triple: str): + self.environ = os.environ.copy() + self.triple = triple + self.is_windows = False + self.is_linux = False + self.is_macos = False + + def set_gstreamer_environment_variables_if_necessary( + self, env: Dict[str, str], cross_compilation_target: Optional[str], check_installation=True + ): + # Environment variables are not needed when cross-compiling on any + # platform other than Windows. UWP doesn't support GStreamer. GStreamer + # for Android is handled elsewhere. + if cross_compilation_target and ( + not self.is_windows + or "uwp" in cross_compilation_target + or "android" in cross_compilation_target + ): + return + + # We may not need to update environment variables if GStreamer is installed + # for the system on Linux. + gstreamer_root = self.gstreamer_root(cross_compilation_target) + if gstreamer_root: + util.prepend_paths_to_env(env, "PATH", os.path.join(gstreamer_root, "bin")) + util.prepend_paths_to_env( + env, "PKG_CONFIG_PATH", os.path.join(gstreamer_root, "lib", "pkgconfig") + ) + util.prepend_paths_to_env( + env, + self.library_path_variable_name(), + os.path.join(gstreamer_root, "lib"), + ) + env["GST_PLUGIN_SCANNER"] = os.path.join( + gstreamer_root, + "libexec", + "gstreamer-1.0", + f"gst-plugin-scanner{self.executable_suffix()}", + ) + env["GST_PLUGIN_SYSTEM_PATH"] = os.path.join(gstreamer_root, "lib", "gstreamer-1.0") + if self.is_macos: + env["OPENSSL_INCLUDE_DIR"] = os.path.join(gstreamer_root, "Headers") + + # If we are not cross-compiling GStreamer must be installed for the system. In + # the cross-compilation case, we might be picking it up from another directory. + if check_installation and not self.is_gstreamer_installed(cross_compilation_target): + raise FileNotFoundError( + "GStreamer libraries not found (>= version 1.16)." + "Please see installation instructions in README.md" + ) + + def gstreamer_root(self, _cross_compilation_target: Optional[str]) -> Optional[str]: + raise NotImplementedError("Do not know how to get GStreamer path for platform.") + + def library_path_variable_name(self): + raise NotImplementedError("Do not know how to set library path for platform.") + + def executable_suffix(self): + return "" + def _platform_bootstrap(self, _cache_dir: str, _force: bool) -> bool: raise NotImplementedError("Bootstrap installation detection not yet available.") - def _platform_bootstrap_gstreamer(self, _cache_dir: str, _force: bool) -> bool: - raise NotImplementedError("GStreamer bootstrap support is not yet available for your OS.") + def _platform_bootstrap_gstreamer(self, _force: bool) -> bool: + raise NotImplementedError( + "GStreamer bootstrap support is not yet available for your OS." + ) - def _platform_is_gstreamer_installed(self) -> bool: - return subprocess.call( - ["pkg-config", "--atleast-version=1.16", "gstreamer-1.0"], - stdout=subprocess.PIPE, stderr=subprocess.PIPE) == 0 + def is_gstreamer_installed(self, cross_compilation_target: Optional[str]) -> bool: + env = os.environ.copy() + self.set_gstreamer_environment_variables_if_necessary( + env, cross_compilation_target, check_installation=False) + return ( + subprocess.call( + ["pkg-config", "--atleast-version=1.16", "gstreamer-1.0"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + env=env, + ) + == 0 + ) def bootstrap(self, cache_dir: str, force: bool): if not self._platform_bootstrap(cache_dir, force): print("Dependencies were already installed!") - def bootstrap_gstreamer(self, cache_dir: str, force: bool): - if not self._platform_bootstrap_gstreamer(cache_dir, force): - print("Dependencies were already installed!") - - def is_gstreamer_installed(self) -> bool: - return self._platform_is_gstreamer_installed() + def bootstrap_gstreamer(self, _cache_dir: str, force: bool): + if not self._platform_bootstrap_gstreamer(force): + root = self.gstreamer_root(None) + if root: + print(f"GStreamer found at: {root}") + else: + print("GStreamer already installed system-wide.") diff --git a/python/servo/platform/linux.py b/python/servo/platform/linux.py index da1a83a3b27..b4a8c031f37 100644 --- a/python/servo/platform/linux.py +++ b/python/servo/platform/linux.py @@ -9,12 +9,12 @@ import os import subprocess - -from typing import Tuple +import tempfile +from typing import Optional, Tuple import distro import six - +from .. import util from .base import Base # Please keep these in sync with the packages in README.md @@ -48,11 +48,21 @@ XBPS_PKGS = ['libtool', 'gcc', 'libXi-devel', 'freetype-devel', 'clang', 'gstreamer1-devel', 'autoconf213', 'gst-plugins-base1-devel', 'gst-plugins-bad1-devel'] +GSTREAMER_URL = \ + "https://github.com/servo/servo-build-deps/releases/download/linux/gstreamer-1.16-x86_64-linux-gnu.20190515.tar.gz" +PREPACKAGED_GSTREAMER_ROOT = \ + os.path.join(util.get_target_dir(), "dependencies", "gstreamer") + class Linux(Base): - def __init__(self): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.is_linux = True (self.distro, self.version) = Linux.get_distro_and_version() + def library_path_variable_name(self): + return "LD_LIBRARY_PATH" + @staticmethod def get_distro_and_version() -> Tuple[str, str]: distrib = six.ensure_str(distro.name()) @@ -116,7 +126,7 @@ class Linux(Base): f"{self.distro}, please file a bug") installed_something = self.install_non_gstreamer_dependencies(force) - installed_something |= self._platform_bootstrap_gstreamer(_cache_dir, force) + installed_something |= self._platform_bootstrap_gstreamer(force) return installed_something def install_non_gstreamer_dependencies(self, force: bool) -> bool: @@ -157,15 +167,34 @@ class Linux(Base): print("Installing missing dependencies...") if run_as_root(command + pkgs, force) != 0: - raise Exception("Installation of dependencies failed.") + raise EnvironmentError("Installation of dependencies failed.") return True - def _platform_bootstrap_gstreamer(self, _cache_dir: str, _force: bool) -> bool: - if self.is_gstreamer_installed(): + def gstreamer_root(self, cross_compilation_target: Optional[str]) -> Optional[str]: + if cross_compilation_target: + return None + if os.path.exists(PREPACKAGED_GSTREAMER_ROOT): + return PREPACKAGED_GSTREAMER_ROOT + # GStreamer might be installed system-wide, but we do not return a root in this + # case because we don't have to update environment variables. + return None + + def _platform_bootstrap_gstreamer(self, force: bool) -> bool: + if not force and self.is_gstreamer_installed(cross_compilation_target=None): return False - gstdir = os.path.join(os.curdir, "support", "linux", "gstreamer") - if not os.path.isdir(os.path.join(gstdir, "gst", "lib")): - subprocess.check_call(["bash", "gstreamer.sh"], cwd=gstdir) + with tempfile.TemporaryDirectory() as temp_dir: + file_name = os.path.join(temp_dir, GSTREAMER_URL.rsplit('/', maxsplit=1)[-1]) + util.download_file("Pre-packaged GStreamer binaries", GSTREAMER_URL, file_name) + + print(f"Installing GStreamer packages to {PREPACKAGED_GSTREAMER_ROOT}...") + os.makedirs(PREPACKAGED_GSTREAMER_ROOT, exist_ok=True) + + # Extract, but strip one component from the output, because the package includes + # a toplevel directory called "./gst/" and we'd like to have the same directory + # structure on all platforms. + subprocess.check_call(["tar", "xf", file_name, "-C", PREPACKAGED_GSTREAMER_ROOT, + "--strip-components=2"]) + + assert self.is_gstreamer_installed(cross_compilation_target=None) return True - return False diff --git a/python/servo/platform/macos.py b/python/servo/platform/macos.py index 6189140cabf..0f5ca632b2f 100644 --- a/python/servo/platform/macos.py +++ b/python/servo/platform/macos.py @@ -9,28 +9,76 @@ import os import subprocess +import tempfile +from typing import Optional +from .. import util from .base import Base -from ..gstreamer import macos_gst_root + +URL_BASE = "https://github.com/servo/servo-build-deps/releases/download/macOS" +GSTREAMER_URL = f"{URL_BASE}/gstreamer-1.0-1.22.2-universal.pkg" +GSTREAMER_DEVEL_URL = f"{URL_BASE}/gstreamer-1.0-devel-1.22.2-universal.pkg" +GSTREAMER_ROOT = "/Library/Frameworks/GStreamer.framework/Versions/1.0" class MacOS(Base): - def __init__(self): - pass - - def _platform_is_gstreamer_installed(self) -> bool: - # We override homebrew gstreamer if installed and always use pkgconfig - # from official gstreamer framework. - try: - gst_root = macos_gst_root() - env = os.environ.copy() - env["PATH"] = os.path.join(gst_root, "bin") - env["PKG_CONFIG_PATH"] = os.path.join(gst_root, "lib", "pkgconfig") - has_gst = subprocess.call( - ["pkg-config", "--atleast-version=1.21", "gstreamer-1.0"], - stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env) == 0 - gst_lib_dir = subprocess.check_output( - ["pkg-config", "--variable=libdir", "gstreamer-1.0"], env=env) - return has_gst and gst_lib_dir.startswith(bytes(gst_root, 'utf-8')) - except FileNotFoundError: + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.is_macos = True + + def library_path_variable_name(self): + return "DYLD_LIBRARY_PATH" + + def gstreamer_root(self, cross_compilation_target: Optional[str]) -> Optional[str]: + # We do not support building with gstreamer while cross-compiling on MacOS. + if cross_compilation_target or not os.path.exists(GSTREAMER_ROOT): + return None + return GSTREAMER_ROOT + + def is_gstreamer_installed(self, cross_compilation_target: Optional[str]) -> bool: + if not super().is_gstreamer_installed(cross_compilation_target): + return False + + # Servo only supports the official GStreamer distribution on MacOS. + env = os.environ.copy() + self.set_gstreamer_environment_variables_if_necessary( + env, cross_compilation_target, check_installation=False + ) + gst_lib_dir = subprocess.check_output( + ["pkg-config", "--variable=libdir", "gstreamer-1.0"], env=env + ) + if not gst_lib_dir.startswith(bytes(GSTREAMER_ROOT, "utf-8")): + print("GStreamer is installed, but not the official packages.\n" + "Run `./mach bootstrap-gtstreamer` or install packages from " + "https://gstreamer.freedesktop.org/") return False + return True + + def _platform_bootstrap_gstreamer(self, force: bool) -> bool: + if not force and self.is_gstreamer_installed(cross_compilation_target=None): + return False + + with tempfile.TemporaryDirectory() as temp_dir: + libs_pkg = os.path.join(temp_dir, GSTREAMER_URL.rsplit("/", maxsplit=1)[-1]) + devel_pkg = os.path.join( + temp_dir, GSTREAMER_DEVEL_URL.rsplit("/", maxsplit=1)[-1] + ) + + util.download_file("GStreamer libraries", GSTREAMER_URL, libs_pkg) + util.download_file( + "GStreamer development support", GSTREAMER_DEVEL_URL, devel_pkg + ) + + print("Installing GStreamer packages...") + subprocess.check_call( + [ + "sudo", + "sh", + "-c", + f"installer -pkg '{libs_pkg}' -target / &&" + f"installer -pkg '{devel_pkg}' -target /", + ] + ) + + assert self.is_gstreamer_installed(cross_compilation_target=None) + return True diff --git a/python/servo/platform/windows.py b/python/servo/platform/windows.py index cea8de5a5dd..8ea2a0a5650 100644 --- a/python/servo/platform/windows.py +++ b/python/servo/platform/windows.py @@ -10,14 +10,15 @@ import os import shutil import subprocess +import tempfile +from typing import Optional import urllib import zipfile - from distutils.version import LooseVersion import six +from .. import util from .base import Base -from ..util import extract, download_file DEPS_URL = "https://github.com/servo/servo-build-deps/releases/download/msvc-deps/" DEPENDENCIES = { @@ -31,10 +32,22 @@ DEPENDENCIES = { "openxr-loader-uwp": "1.0", } +URL_BASE = "https://gstreamer.freedesktop.org/data/pkg/windows/1.16.0/" +GSTREAMER_URL = f"{URL_BASE}/gstreamer-1.0-msvc-x86_64-1.16.0.msi" +GSTREAMER_DEVEL_URL = f"{URL_BASE}/gstreamer-1.0-devel-msvc-x86_64-1.16.0.msi" +DEPENDENCIES_DIR = os.path.join(util.get_target_dir(), "dependencies") + class Windows(Base): - def __init__(self): - pass + def __init__(self, triple: str): + super().__init__(triple) + self.is_windows = True + + def executable_suffix(self): + return ".exe" + + def library_path_variable_name(self): + return "LIB" @staticmethod def cmake_already_installed(required_version: str) -> bool: @@ -51,11 +64,11 @@ class Windows(Base): def prepare_file(cls, deps_dir: str, zip_path: str, full_spec: str): if not os.path.isfile(zip_path): zip_url = "{}{}.zip".format(DEPS_URL, urllib.parse.quote(full_spec)) - download_file(full_spec, zip_url, zip_path) + util.download_file(full_spec, zip_url, zip_path) - print("Extracting {}...".format(full_spec), end='') + print("Extracting {}...".format(full_spec), end="") try: - extract(zip_path, deps_dir) + util.extract(zip_path, deps_dir) except zipfile.BadZipfile: print("\nError: %s.zip is not a valid zip file, redownload..." % full_spec) os.remove(zip_path) @@ -70,7 +83,7 @@ class Windows(Base): return os.path.join(deps_dir, package, version) to_install = {} - for (package, version) in DEPENDENCIES.items(): + for package, version in DEPENDENCIES.items(): # Don't install CMake if it already exists in PATH if package == "cmake" and self.cmake_already_installed(version): continue @@ -82,8 +95,8 @@ class Windows(Base): return False print("Installing missing MSVC dependencies...") - for (package, version) in to_install.items(): - full_spec = '{}-{}'.format(package, version) + for package, version in to_install.items(): + full_spec = "{}-{}".format(package, version) package_dir = get_package_dir(package, version) parent_dir = os.path.dirname(package_dir) @@ -96,3 +109,67 @@ class Windows(Base): os.rename(extracted_path, package_dir) return True + + def gstreamer_root(self, cross_compilation_target: Optional[str]) -> Optional[str]: + build_target_triple = cross_compilation_target or self.triple + gst_arch_names = { + "x86_64": "X86_64", + "x86": "X86", + "aarch64": "ARM64", + } + gst_arch_name = gst_arch_names[build_target_triple.split("-")[0]] + + # The bootstraped version of GStreamer always takes precedance of the installed vesion. + prepackaged_root = os.path.join( + DEPENDENCIES_DIR, "gstreamer", "1.0", gst_arch_name + ) + if os.path.exists(os.path.join(prepackaged_root, "bin", "ffi-7.dll")): + return prepackaged_root + + # The installed version of GStreamer often sets an environment variable pointing to + # the install location. + root_from_env = os.environ.get(f"GSTREAMER_1_0_ROOT_{gst_arch_name}") + if root_from_env: + return root_from_env + + # If all else fails, look for an installation in the default install directory. + default_root = os.path.join("C:\\gstreamer\\1.0", gst_arch_name) + if os.path.exists(os.path.join(default_root, "bin", "ffi-7.dll")): + return default_root + + return None + + def is_gstreamer_installed(self, cross_compilation_target: Optional[str]) -> bool: + return self.gstreamer_root(cross_compilation_target) is not None + + def _platform_bootstrap_gstreamer(self, force: bool) -> bool: + if not force and self.is_gstreamer_installed(cross_compilation_target=None): + return False + + if "x86_64" not in self.triple: + print("Bootstrapping gstreamer not supported on " + "non-x86-64 Windows. Please install manually") + return False + + with tempfile.TemporaryDirectory() as temp_dir: + libs_msi = os.path.join(temp_dir, GSTREAMER_URL.rsplit("/", maxsplit=1)[-1]) + devel_msi = os.path.join( + temp_dir, GSTREAMER_DEVEL_URL.rsplit("/", maxsplit=1)[-1] + ) + + util.download_file("GStreamer libraries", GSTREAMER_URL, libs_msi) + util.download_file( + "GStreamer development support", GSTREAMER_DEVEL_URL, devel_msi + ) + + print(f"Installing GStreamer packages to {DEPENDENCIES_DIR}...") + os.makedirs(DEPENDENCIES_DIR, exist_ok=True) + common_args = [ + f"TARGETDIR={DEPENDENCIES_DIR}", # Install destination + "/qn", # Quiet mode + ] + subprocess.check_call(["msiexec", "/a", libs_msi] + common_args) + subprocess.check_call(["msiexec", "/a", devel_msi] + common_args) + + assert self.is_gstreamer_installed(cross_compilation_target=None) + return True diff --git a/python/servo/post_build_commands.py b/python/servo/post_build_commands.py index 74af93fb0c9..07f104630a1 100644 --- a/python/servo/post_build_commands.py +++ b/python/servo/post_build_commands.py @@ -15,6 +15,8 @@ import os.path as path import subprocess from shutil import copytree, rmtree, copy2 +import servo.util + from mach.decorators import ( CommandArgument, CommandProvider, @@ -79,9 +81,15 @@ class PostBuildCommands(CommandBase): help="Command-line arguments to be passed through to Servo") def run(self, params, release=False, dev=False, android=None, debug=False, debugger=None, headless=False, software=False, bin=None, emulator=False, usb=False, nightly=None): - self.set_run_env(android is not None) env = self.build_env() env["RUST_BACKTRACE"] = "1" + if software: + if not is_linux(): + print("Software rendering is only supported on Linux at the moment.") + return + + env['LIBGL_ALWAYS_SOFTWARE'] = "1" + os.environ.update(env) # Make --debugger imply --debug if debugger: @@ -129,13 +137,6 @@ class PostBuildCommands(CommandBase): if headless: args.append('-z') - if software: - if not is_linux(): - print("Software rendering is only supported on Linux at the moment.") - return - - env['LIBGL_ALWAYS_SOFTWARE'] = "1" - # Borrowed and modified from: # http://hg.mozilla.org/mozilla-central/file/c9cfa9b91dea/python/mozbuild/mozbuild/mach_commands.py#l883 if debug: @@ -251,7 +252,7 @@ class PostBuildCommands(CommandBase): toolchain_path = path.dirname(path.dirname(rustc_path)) rust_docs = path.join(toolchain_path, "share", "doc", "rust", "html") - docs = path.join(self.get_target_dir(), "doc") + docs = path.join(servo.util.get_target_dir(), "doc") if not path.exists(docs): os.makedirs(docs) @@ -293,4 +294,4 @@ class PostBuildCommands(CommandBase): self.doc([]) import webbrowser webbrowser.open("file://" + path.abspath(path.join( - self.get_target_dir(), "doc", "servo", "index.html"))) + servo.util.get_target_dir(), "doc", "servo", "index.html"))) diff --git a/python/servo/testing_commands.py b/python/servo/testing_commands.py index 18780b2ac01..f632ce30a1c 100644 --- a/python/servo/testing_commands.py +++ b/python/servo/testing_commands.py @@ -40,7 +40,6 @@ from servo.command_base import ( call, check_call, check_output, ) from servo_tidy_tests import test_tidy -from servo.platform import host_triple SCRIPT_PATH = os.path.split(__file__)[0] PROJECT_TOPLEVEL_PATH = os.path.abspath(os.path.join(SCRIPT_PATH, "..", "..")) @@ -247,16 +246,6 @@ class MachCommands(CommandBase): packages.discard('stylo') - env = self.build_env(test_unit=True) - # FIXME: https://github.com/servo/servo/issues/26192 - if "apple-darwin" not in host_triple(): - env["RUST_BACKTRACE"] = "1" - - if "msvc" in host_triple(): - # on MSVC, we need some DLLs in the path. They were copied - # in to the servo.exe build dir, so just point PATH to that. - env["PATH"] = "%s%s%s" % (path.dirname(self.get_binary_path(False, False)), os.pathsep, env["PATH"]) - if len(packages) > 0 or len(in_crate_packages) > 0: args = [] for crate in packages: @@ -270,7 +259,7 @@ class MachCommands(CommandBase): err = self.run_cargo_build_like_command("bench" if bench else "test", args, - env=env, + env=self.build_env(test_unit=True), with_layout_2020=with_layout_2020, **kwargs) if err: @@ -393,7 +382,8 @@ class MachCommands(CommandBase): return self._test_wpt(android=True, **kwargs) def _test_wpt(self, android=False, **kwargs): - self.set_run_env(android) + if not android: + os.environ.update(self.build_env()) return wpt.run.run_tests(**kwargs) # Helper to ensure all specified paths are handled, otherwise dispatch to appropriate test suite. diff --git a/python/servo/util.py b/python/servo/util.py index 4151bbd4929..43769e181de 100644 --- a/python/servo/util.py +++ b/python/servo/util.py @@ -17,9 +17,12 @@ import stat import sys import time import urllib +import urllib.request import zipfile +from typing import Dict, List, Union -from io import BytesIO +import six +from io import BufferedIOBase, BytesIO from socket import error as socket_error try: @@ -27,6 +30,8 @@ try: except ImportError: HAS_SNI = False +SCRIPT_PATH = os.path.abspath(os.path.dirname(__file__)) +SERVO_ROOT = os.path.abspath(os.path.join(SCRIPT_PATH, "..", "..")) HAS_SNI_AND_RECENT_PYTHON = HAS_SNI and sys.version_info >= (2, 7, 9) @@ -58,17 +63,17 @@ def delete(path): os.remove(path) -def download(desc, src, writer, start_byte=0): +def download(description: str, url: str, writer: BufferedIOBase, start_byte: int = 0): if start_byte: - print("Resuming download of {} ...".format(src)) + print("Resuming download of {} ...".format(url)) else: - print("Downloading {} ...".format(src)) + print("Downloading {} ...".format(url)) dumb = (os.environ.get("TERM") == "dumb") or (not sys.stdout.isatty()) try: - req = urllib.request.Request(src) + req = urllib.request.Request(url) if start_byte: - req = urllib.request.Request(src, headers={'Range': 'bytes={}-'.format(start_byte)}) + req = urllib.request.Request(url, headers={'Range': 'bytes={}-'.format(start_byte)}) resp = urllib.request.urlopen(req, **get_urlopen_kwargs()) fsize = None @@ -79,7 +84,7 @@ def download(desc, src, writer, start_byte=0): chunk_size = 64 * 1024 previous_progress_line = None - previous_progress_line_time = 0 + previous_progress_line_time = 0.0 while True: chunk = resp.read(chunk_size) if not chunk: @@ -88,7 +93,7 @@ def download(desc, src, writer, start_byte=0): if not dumb: if fsize is not None: pct = recved * 100.0 / fsize - progress_line = "\rDownloading %s: %5.1f%%" % (desc, pct) + progress_line = "\rDownloading %s: %5.1f%%" % (description, pct) now = time.time() duration = now - previous_progress_line_time if progress_line != previous_progress_line and duration > .1: @@ -102,13 +107,13 @@ def download(desc, src, writer, start_byte=0): if not dumb: print() except urllib.error.HTTPError as e: - print("Download failed ({}): {} - {}".format(e.code, e.reason, src)) + print("Download failed ({}): {} - {}".format(e.code, e.reason, url)) if e.code == 403: print("No Rust compiler binary available for this platform. " "Please see https://github.com/servo/servo/#prerequisites") sys.exit(1) except urllib.error.URLError as e: - print("Error downloading {}: {}. The failing URL was: {}".format(desc, e.reason, src)) + print("Error downloading {}: {}. The failing URL was: {}".format(description, e.reason, url)) sys.exit(1) except socket_error as e: print("Looks like there's a connectivity issue, check your Internet connection. {}".format(e)) @@ -118,22 +123,22 @@ def download(desc, src, writer, start_byte=0): raise -def download_bytes(desc, src): +def download_bytes(description: str, url: str): content_writer = BytesIO() - download(desc, src, content_writer) + download(description, url, content_writer) return content_writer.getvalue() -def download_file(desc, src, dst): - tmp_path = dst + ".part" +def download_file(description: str, url: str, destination_path: str): + tmp_path = destination_path + ".part" try: start_byte = os.path.getsize(tmp_path) with open(tmp_path, 'ab') as fd: - download(desc, src, fd, start_byte=start_byte) + download(description, url, fd, start_byte=start_byte) except os.error: with open(tmp_path, 'wb') as fd: - download(desc, src, fd) - os.rename(tmp_path, dst) + download(description, url, fd) + os.rename(tmp_path, destination_path) # https://stackoverflow.com/questions/39296101/python-zipfile-removes-execute-permissions-from-binaries @@ -198,3 +203,30 @@ def check_hash(filename, expected, algorithm): def get_default_cache_dir(topdir): return os.environ.get("SERVO_CACHE_DIR", os.path.join(topdir, ".servo")) + + +def append_paths_to_env(env: Dict[str, str], key: str, paths: Union[str, List[str]]): + if isinstance(paths, list): + paths = os.pathsep.join(paths) + + existing_value = env.get(key, None) + if existing_value: + new_value = six.ensure_str(existing_value) + os.pathsep + paths + else: + new_value = paths + env[key] = new_value + + +def prepend_paths_to_env(env: Dict[str, str], key: str, paths: Union[str, List[str]]): + if isinstance(paths, list): + paths = os.pathsep.join(paths) + + existing_value = env.get(key, None) + new_value = paths + if existing_value: + new_value += os.pathsep + six.ensure_str(existing_value) + env[key] = new_value + + +def get_target_dir(): + return os.environ.get("CARGO_TARGET_DIR", os.path.join(SERVO_ROOT, "target")) |