blob: ea0de5a4ca2e8552280ee78d127e857788fad927 [file] [log] [blame]
# -*- coding: utf-8 -*-
# Copyright (C) 2017 Igalia S.L.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program; if not, write to the
# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
# Boston, MA 02110-1301, USA.
import argparse
import atexit
import logging
try:
import configparser
except ImportError:
import ConfigParser as configparser
from contextlib import contextmanager
import errno
import json
import multiprocessing
import os
import shlex
import shutil
import signal
import subprocess
import sys
import tempfile
import re
import platform
SCRIPT_DIR = os.path.abspath(os.path.dirname(__file__))
WEBKIT_SOURCE_DIR = os.path.normpath(os.path.abspath(os.path.join(SCRIPT_DIR, "..", "..")))
sys.path.insert(0, os.path.join(WEBKIT_SOURCE_DIR, "Tools", "Scripts"))
from webkitpy.common.system.logutils import configure_logging
from webkitcorepy import string_utils
import toml
import json
try:
from urllib.parse import urlparse # pylint: disable=E0611
except ImportError:
from urlparse import urlparse
try:
from urllib.request import urlopen # pylint: disable=E0611
except ImportError:
from urllib2 import urlopen
try:
from contextlib import nullcontext
except ImportError:
@contextmanager
def nullcontext(enter_result=None):
yield enter_result
FLATPAK_REQUIRED_VERSION = "1.4.4"
_log = logging.getLogger(__name__)
BUILD_ROOT_DIR_NAME = 'WebKitBuild'
# This path doesn't take $WEBKIT_OUTPUTDIR in account because the standalone toolchains
# paths depend on it and those are also hard-coded in the generated sccache config.
DEFAULT_BUILD_ROOT = os.path.join(WEBKIT_SOURCE_DIR, BUILD_ROOT_DIR_NAME)
BUILD_ROOT = os.path.join(os.environ.get("WEBKIT_OUTPUTDIR", WEBKIT_SOURCE_DIR), BUILD_ROOT_DIR_NAME)
FLATPAK_USER_DIR_PATH = os.path.realpath(os.path.join(DEFAULT_BUILD_ROOT, "UserFlatpak"))
DEFAULT_SCCACHE_SCHEDULER='https://sccache.igalia.com'
# Where the source folder is mounted inside the sandbox.
SANDBOX_SOURCE_ROOT = "/app/webkit"
# Our SDK branch matches with the FDO SDK branch. When updating the FDO SDK release branch
# in our SDK build definitions please don't forget to update the version here as well.
SDK_BRANCH = "21.08"
WEBKIT_SDK_FLATPAK_REPO_URL = "https://software.igalia.com/flatpak-refs/webkit-sdk.flatpakrepo"
WEBKIT_SDK_GPG_PUBKEY_URL = "https://software.igalia.com/flatpak-refs/webkit-sdk-pubkey.gpg"
WEBKIT_SDK_REPO_URL = "https://software.igalia.com/webkit-sdk-repo/"
is_colored_output_supported = False
try:
import curses
assert sys.stdout.isatty()
curses.setupterm()
assert curses.tigetnum("colors") > 0
except Exception:
is_colored_output_supported = False
else:
is_colored_output_supported = True
class Colors:
HEADER = "\033[95m"
OKBLUE = "\033[94m"
OKGREEN = "\033[92m"
WARNING = "\033[93m"
FAIL = "\033[91m"
ENDC = "\033[0m"
class Console:
quiet = False
@classmethod
def message(cls, str_format, *args):
if cls.quiet:
return
if args:
print(str_format % args)
else:
print(str_format)
# Flush so that messages are printed at the right time
# as we use many subprocesses.
sys.stdout.flush()
@classmethod
def colored_message_if_supported(cls, color, str_format, *args):
if args:
msg = str_format % args
else:
msg = str_format
if is_colored_output_supported:
cls.message("\n%s%s%s", color, msg, Colors.ENDC)
else:
cls.message(msg)
@classmethod
def error_message(cls, str_format, *args):
cls.colored_message_if_supported(Colors.FAIL, str_format, *args)
@classmethod
def warning_message(cls, str_format, *args):
cls.colored_message_if_supported(Colors.WARNING, str_format, *args)
def run_sanitized(command, gather_output=False, ignore_stderr=False, env=None, **kwargs):
""" Runs a command in a santized environment and optionally returns decoded output or raises
subprocess.CalledProcessError
"""
if env:
sanitized_env = env.copy()
else:
sanitized_env = os.environ.copy()
# We need clean output free of debug messages
try:
del sanitized_env["G_MESSAGES_DEBUG"]
except KeyError:
pass
_log.debug("Running %s", " ".join(command))
keywords = kwargs
keywords['env'] = sanitized_env
if gather_output:
if ignore_stderr:
with open(os.devnull, 'w') as devnull:
output = subprocess.check_output(command, stderr=devnull, **keywords)
else:
output = subprocess.check_output(command, **keywords)
return output.strip().decode('utf-8')
else:
keywords["stdout"] = sys.stdout
if ignore_stderr:
with open(os.devnull, 'w') as devnull:
keywords["stderr"] = devnull
return subprocess.check_call(command, **keywords)
else:
return subprocess.check_call(command, **keywords)
def check_flatpak(verbose=True):
# Flatpak is only supported on Linux.
if not sys.platform.startswith("linux"):
return ()
required_version = FLATPAK_REQUIRED_VERSION
try:
output = run_sanitized(["flatpak", "--version"], gather_output=True)
except (subprocess.CalledProcessError, OSError):
if verbose:
Console.error_message("You need to install flatpak >= %s"
" to be able to use the '%s' script.\n\n"
"You can find some informations about"
" how to install it for your distribution at:\n"
" * https://flatpak.org/\n", required_version,
sys.argv[0])
return ()
def comparable_version(version):
return tuple(map(int, (version.split("."))))
version = output.split(" ")[1].strip("\n")
current_version = comparable_version(version)
if current_version < comparable_version(required_version):
Console.error_message("flatpak %s required but %s found. Please update and try again\n",
required_version, version)
return ()
return current_version
def convert_webkit_source_path_to_sandbox_path(source_path):
'''Convert a path in the WebKit source directory to the same path in the
sandboxed source diretory. '''
return source_path.replace(WEBKIT_SOURCE_DIR, SANDBOX_SOURCE_ROOT)
def convert_sandbox_path_to_webkit_source_path(sandbox_path):
# For now this supports only files in the /app/webkit path
return sandbox_path.replace(SANDBOX_SOURCE_ROOT, WEBKIT_SOURCE_DIR)
def get_build_dir(platform, build_type):
return os.path.join(BUILD_ROOT, platform, build_type)
class FlatpakObject:
def __init__(self, user):
self.user = user
def flatpak(self, command, *args, **kwargs):
comment = kwargs.pop("comment", None)
gather_output = kwargs.get("gather_output", False)
if comment:
Console.message(comment)
command = ["flatpak", command]
help_output = run_sanitized(command + ["--help"], gather_output=True)
if self.user and "--user" in help_output:
command.append("--user")
if "--assumeyes" in help_output:
command.append("--assumeyes")
if "--noninteractive" in help_output and gather_output:
command.append("--noninteractive")
command.extend(args)
return run_sanitized(command, **kwargs)
def version(self, ref_id):
try:
output = self.flatpak("info", ref_id, gather_output=True, ignore_stderr=True)
except subprocess.CalledProcessError:
# ref is likely not installed
return ""
for line in output.splitlines():
tokens = line.split(":")
if len(tokens) != 2:
continue
if tokens[0].strip().lower() == "version":
return tokens[1].strip()
return ""
def flatpak_update(self):
remote = "webkit-sdk"
try:
self.flatpak("remote-ls", remote, gather_output=True)
except subprocess.CalledProcessError as error:
if error.output.lower().find(b"key expired"):
Console.message("WebKit SDK GPG key expired, synchronizing with remote")
with tempfile.NamedTemporaryFile() as tmpfile:
fd = urlopen(WEBKIT_SDK_GPG_PUBKEY_URL)
tmpfile.write(fd.read())
tmpfile.flush()
self.flatpak("remote-modify", "--gpg-import=" + tmpfile.name, remote)
self.flatpak("update", comment="Updating Flatpak environment")
class FlatpakPackages(FlatpakObject):
def __init__(self, repos, user=True):
FlatpakObject.__init__(self, user=user)
self.repos = repos
self.update()
def update(self):
self.packages = []
out = self.flatpak("list", "--columns=application,arch,branch,origin", "-a", gather_output=True)
package_defs = [line for line in out.split("\n") if line]
for package_def in package_defs:
name, arch, branch, origin = package_def.split("\t")
# If installed from a file, the package is in no repo
repo = self.repos.repos.get(origin)
self.packages.append(FlatpakPackage(name, branch, repo, arch))
def __iter__(self):
for package in self.packages:
yield package
class FlatpakRepos(FlatpakObject):
def __init__(self, repos, user=True):
FlatpakObject.__init__(self, user=user)
self.update()
updated = False
for repo in repos:
updated = self.add(repo, update=False) or updated
# Only fetch the remote and package list again if we updated anything.
if updated:
self.update()
def update(self):
self.repos = {}
out = self.flatpak("remote-list", "--columns=name,title,url", gather_output=True)
remotes = [line for line in out.split("\n") if line]
for remote in remotes:
name, title, url = remote.split("\t")
parsed_url = urlparse(url)
if not parsed_url.scheme:
Console.message("No valid URI found for: %s", remote)
continue
self.repos[name] = FlatpakRepo(name=name, url=url, desc=title, repos=self)
self.packages = FlatpakPackages(self)
def add(self, repo, update=True, override=True):
try:
repo.repos = self
same_name = None
for name, tmprepo in self.repos.items():
if repo.url == tmprepo.url:
return False
elif repo.name == name:
same_name = tmprepo
if same_name:
if override and same_name.url != repo.url:
self.flatpak("remote-modify", repo.name, "--url=" + repo.url)
same_name.url = repo.url
return True
else:
return False
args = ["remote-add", repo.name, "--if-not-exists"]
if repo.repo_file:
args.extend(["--from", repo.repo_file.name])
else:
args.extend(["--no-gpg-verify", repo.url])
self.flatpak(*args, comment="Adding repo %s" % repo.name)
return True
finally:
if update:
self.packages = FlatpakPackages(self)
def is_package_installed(self, name, branch=None, arch=None):
for package in self.packages:
if name != package.name:
continue
if branch and branch != package.branch:
continue
if arch and arch != package.arch:
continue
return True
return False
class FlatpakRepo(FlatpakObject):
def __init__(self, name, desc=None, url=None,
repo_file=None, user=True, repos=None):
FlatpakObject.__init__(self, user=user)
self.name = name
self.url = url
self.desc = desc
self.repo_file_name = repo_file
self._repo_file = None
self.repos = repos
assert name
if repo_file and not url:
repo = configparser.ConfigParser()
repo.read(self.repo_file.name)
try:
self.url = repo["Flatpak Repo"]["Url"]
except AttributeError:
self.url = repo.get("Flatpak Repo", "Url")
else:
assert url
@property
def repo_file(self):
if self._repo_file:
return self._repo_file
if not self.repo_file_name:
return None
self._repo_file = tempfile.NamedTemporaryFile(mode="wb")
self._repo_file.write(urlopen(self.repo_file_name).read())
self._repo_file.flush()
return self._repo_file
class FlatpakPackage(FlatpakObject):
"""A flatpak app."""
def __init__(self, name, branch, repo, arch, user=True, hash=None):
FlatpakObject.__init__(self, user=user)
self.name = name
self.branch = str(branch)
self.repo = repo
self.arch = arch
def __repr__(self):
return "%s/%s/%s %s" % (self.name, self.arch, self.branch, self.repo.name)
def __str__(self):
return "%s/%s/%s" % (self.name, self.arch, self.branch)
def is_installed(self, branch):
# Bundles installed from files do not have repositories.
if not self.repo:
return True
return self.repo.repos.is_package_installed(self.name, branch, self.arch)
def install(self):
if not self.repo:
return False
branch = self.branch
args = ("install", self.repo.name, self.name, "--reinstall", branch)
comment = "Installing from " + self.repo.name + " " + self.name + " " + self.arch + " " + branch
self.flatpak(*args, comment=comment)
self.repo.repos.packages.update()
@contextmanager
def disable_signals(signals=[signal.SIGINT, signal.SIGTERM, signal.SIGHUP]):
old_signal_handlers = []
for disabled_signal in signals:
handler = signal.getsignal(disabled_signal)
if handler:
old_signal_handlers.append((disabled_signal, handler))
signal.signal(disabled_signal, signal.SIG_IGN)
yield
for disabled_signal, previous_handler in old_signal_handlers:
signal.signal(disabled_signal, previous_handler)
def extract_extra_command_args(args):
"""Takes a list of unparsed args and splits them at '--' to pass to subprocesses."""
try:
return args[args.index('--') + 1:]
except ValueError:
return []
class WebkitFlatpak:
@staticmethod
def load_from_args(args=None, add_help=True):
self = WebkitFlatpak()
parser = argparse.ArgumentParser(prog="webkit-flatpak", add_help=add_help)
general = parser.add_argument_group("General")
general.add_argument('--verbose', action='store_true', help='Show debug messages')
general.add_argument('--version', action='store_true', help='Show SDK version', dest="show_version")
type_group = parser.add_mutually_exclusive_group()
type_group.add_argument("--debug",
help="Compile with Debug configuration, also installs Sdk debug symbols.",
dest='build_type', action="store_const", const="Debug")
type_group.add_argument("--release", help="Compile with Release configuration.",
dest='build_type', action="store_const", const="Release")
general.add_argument('--gtk', action='store_const', dest='platform', const='gtk',
help='Setup build directory for the GTK port')
general.add_argument('--wpe', action='store_const', dest='platform', const='wpe',
help=('Setup build directory for the WPE port'))
general.add_argument("-u", "--update", dest="update",
action="store_true",
help="Update the SDK")
general.add_argument("-bdeps", "--build-local-deps", dest="build_local_deps",
action="store_true", help="Force rebuilding local dependencies")
general.add_argument("-q", "--quiet", dest="quiet",
action="store_true",
help="Do not print anything")
general.add_argument("-c", "--command",
nargs=argparse.REMAINDER,
help="The command to run in the sandbox",
dest="user_command")
general.add_argument('--available', action='store_true', dest="check_available", help='Check if required dependencies are available.'),
general.add_argument("--repo", help="Filesystem absolute path to the Flatpak repository to use", dest="user_repo")
distributed_build_options = parser.add_argument_group("Distributed building")
distributed_build_options.add_argument("--use-icecream", dest="use_icecream", help="Use the distributed icecream (icecc) compiler.", action="store_true")
distributed_build_options.add_argument("-r", "--regenerate-toolchains", dest="regenerate_toolchains", action="store_true",
help="Regenerate IceCC/SCCache standalone toolchain archives")
distributed_build_options.add_argument("-t", "--sccache-token", dest="sccache_token",
help="sccache authentication token")
distributed_build_options.add_argument("-s", "--sccache-scheduler", dest="sccache_scheduler",
help="sccache scheduler URL (default: %s)" % DEFAULT_SCCACHE_SCHEDULER)
debugoptions = parser.add_argument_group("Debugging")
debugoptions.add_argument("--gdb", nargs="?", help="Activate gdb, passing extra args to it if wanted.")
debugoptions.add_argument("--gdb-stack-trace", dest="gdb_stack_trace", nargs="?",
help="Dump the stacktrace to stdout. The argument is a timestamp to be parsed by coredumpctl.")
debugoptions.add_argument("-m", "--coredumpctl-matches", default="", help='Arguments to pass to gdb.')
buildoptions = parser.add_argument_group("Extra build arguments")
buildoptions.add_argument("--cmakeargs",
help="One or more optional CMake flags (e.g. --cmakeargs=\"-DFOO=bar -DCMAKE_PREFIX_PATH=/usr/local\")")
parsing_namespace, self.args = parser.parse_known_args(args=args, namespace=self)
self.extra_command_args = extract_extra_command_args(parsing_namespace.args)
if not self.build_type:
self.build_type = "Release"
if os.environ.get('CCACHE_PREFIX') == 'icecc':
self.use_icecream = True
verbose = os.environ.get('WEBKIT_FLATPAK_SDK_VERBOSE')
if (not self.verbose) and (verbose is not None):
self.verbose = verbose != '0'
configure_logging(logging.DEBUG if self.verbose else logging.INFO)
if self.user_repo:
if not os.path.exists(self.user_repo):
_log.error('User repo at %s is not accessible\n' % self.user_repo)
sys.exit(1)
return self
def __init__(self):
self.sdk_repo = None
self.runtime = None
self.sdk = None
self.user_repo = None
self.show_version = False
self.verbose = False
self.quiet = False
self.update = False
self.args = []
self.gdb_stack_trace = False
self.release = False
self.debug = False
self.build_local_deps = False
self.platform = "gtk"
self.check_available = False
self.user_command = []
# debug options
self.gdb = None
self.coredumpctl_matches = ""
# Extra build options
self.cmakeargs = ""
self.use_icecream = False
self.icc_version = {}
self.regenerate_toolchains = False
self.sccache_token = ""
self.sccache_scheduler = DEFAULT_SCCACHE_SCHEDULER
self.dbus_proxy_process = None
def __del__(self):
if self.dbus_proxy_process:
self.dbus_proxy_process.kill()
def execute_command(self, args, stdout=None, stderr=None, env=None, keep_signals=True):
if keep_signals:
ctx_manager = nullcontext()
else:
ctx_manager = disable_signals()
_log.debug('Running: %s\n' % ' '.join(string_utils.decode(arg) for arg in args))
result = 0
with ctx_manager:
try:
result = subprocess.check_call(args, stdout=stdout, stderr=stderr, env=env)
except subprocess.CalledProcessError as err:
if self.verbose:
cmd = ' '.join(string_utils.decode(arg) for arg in err.cmd)
message = "'%s' returned a non-zero exit code." % cmd
if stderr:
with open(stderr.name, 'r') as stderrf:
message += " Stderr: %s" % stderrf.read()
Console.error_message(message)
return err.returncode
return result
def clean_args(self):
if self.user_repo:
os.environ["FLATPAK_USER_DIR"] = FLATPAK_USER_DIR_PATH + ".Local"
else:
os.environ["FLATPAK_USER_DIR"] = os.environ.get("WEBKIT_FLATPAK_USER_DIR", FLATPAK_USER_DIR_PATH)
self.flatpak_build_path = os.environ["FLATPAK_USER_DIR"]
try:
os.makedirs(self.flatpak_build_path)
except OSError as e:
pass
_log.debug("Using flatpak user dir: %s" % self.flatpak_build_path)
self.platform = self.platform.upper()
if self.gdb is None and '--gdb' in sys.argv:
self.gdb = True
self.config_file = os.path.join(self.flatpak_build_path, 'webkit_flatpak_config.json')
self.sccache_config_file = os.path.join(self.flatpak_build_path, 'sccache.toml')
self.build_path = get_build_dir(self.platform, self.build_type)
_log.debug("Building %s port in %s" % (self.platform, self.build_path))
self.toolchains_directory = os.path.join(self.flatpak_build_path, "Toolchains")
if not os.path.isdir(self.toolchains_directory):
os.makedirs(self.toolchains_directory)
Console.quiet = self.quiet
self.flatpak_version = check_flatpak()
if not self.flatpak_version:
return False
self._reset_repository()
try:
with open(self.config_file) as config:
json_config = json.load(config)
self.icc_version = json_config['icecc_version']
except IOError as e:
pass
return True
def _reset_repository(self):
url = WEBKIT_SDK_REPO_URL
repo_file = WEBKIT_SDK_FLATPAK_REPO_URL
if self.user_repo:
url = "file://%s" % self.user_repo
repo_file = None
self.sdk_repo = FlatpakRepo("webkit-sdk", url=url, repo_file=repo_file)
self.flathub_repo = FlatpakRepo("flathub", url="https://dl.flathub.org/repo/",
repo_file="https://dl.flathub.org/repo/flathub.flatpakrepo")
self.repos = FlatpakRepos([self.sdk_repo, self. flathub_repo])
def setup_builddir(self):
if os.path.exists(os.path.join(self.flatpak_build_path, "metadata")):
return True
if not self.check_installed_packages():
return False
self.sdk_repo.flatpak("build-init",
self.flatpak_build_path,
"org.webkit.Webkit",
str(self.sdk),
str(self.runtime),
self.sdk.branch)
return True
def setup_local_deps(self, building):
if not os.environ.get('WEBKIT_SDK_LOCAL_DEPS'):
if building:
_log.debug("$WEBKIT_SDK_LOCAL_DEPS environment variable not set. Skipping local dependencies build")
return {}
src_dir = os.path.join(WEBKIT_SOURCE_DIR, 'Tools', 'flatpak', 'local-projects')
build_dir = os.path.join(DEFAULT_BUILD_ROOT, 'deps-build')
sandbox_build_dir = convert_webkit_source_path_to_sandbox_path(build_dir)
if not os.path.exists(os.path.join(build_dir, 'build.ninja')):
if not building:
raise RuntimeError('Trying to enter deps-build env from %s but it is not built, make sure to rebuild webkit.', src_dir)
projects = '-Dsubprojects=%s' % os.environ['WEBKIT_SDK_LOCAL_DEPS']
options = shlex.split(os.environ.get('WEBKIT_SDK_LOCAL_DEPS_OPTIONS', ''))
args = ['meson', projects]
args.extend(options + [convert_webkit_source_path_to_sandbox_path(src_dir), sandbox_build_dir])
self.run_in_sandbox(*args, building_local_deps=True, start_sccache=False)
if building:
Console.message("Building local dependencies from %s ", src_dir)
if self.run_in_sandbox('meson', 'compile', '-C', sandbox_build_dir, building_local_deps=True, start_sccache=False) != 0:
raise RuntimeError('Error while building local dependencies.')
command = ['meson', 'devenv', '-C', sandbox_build_dir, '--dump']
local_env = self.run_in_sandbox(*command, building_local_deps=True, start_sccache=False, gather_output=True)
env = {}
for line in [line for line in local_env.splitlines() if not line.startswith("export")]:
tokens = line.split("=")
var_name, contents = tokens[0], "=".join(tokens[1:])
env[var_name] = contents
return env
def _merge_env_variables(self, environment, additional_environment):
for var_name, value in additional_environment.items():
if var_name not in environment:
environment[var_name] = value
else:
if var_name.endswith('PATH'):
environment[var_name] = "%s:%s" % (environment[var_name], value)
return environment
def is_branch_build(self):
try:
with open(os.devnull, 'w') as devnull:
rev_parse = subprocess.check_output(("git", "rev-parse", "--abbrev-ref", "HEAD"), stderr=devnull)
except subprocess.CalledProcessError:
# This is likely not a git checkout.
return False
git_branch_name = rev_parse.decode("utf-8").strip()
for option_name in ("branch.%s.webKitBranchBuild" % git_branch_name,
"webKitBranchBuild"):
try:
with open(os.devnull, 'w') as devnull:
output = subprocess.check_output(("git", "config", "--bool", option_name), stderr=devnull).strip()
except subprocess.CalledProcessError:
continue
if output == "true":
return True
return False
def is_build_webkit(self, command):
return command and "build-webkit" in os.path.basename(command)
def is_build_jsc(self, command):
return command and "build-jsc" in os.path.basename(command)
def setup_a11y_proxy(self):
try:
output = subprocess.check_output(("gdbus", "call", "-e", "-d", "org.a11y.Bus", "-o", "/org/a11y/bus", "-m", "org.a11y.Bus.GetAddress"))
a11y_bus_address = re.findall(br"'([^']+)", output)[0] # Extract string from output from: ('unix:abstract=0000f',)
_log.debug("Found a11y address {}".format(a11y_bus_address))
except (subprocess.CalledProcessError, IndexError, FileNotFoundError) as e:
_log.warning("Failed to get a11y address {}".format(e))
return []
dbus_proxy_path = shutil.which("xdg-dbus-proxy")
if not dbus_proxy_path:
_log.warning("Failed to find xdg-dbus-proxy. Can't forward a11y bus.")
return []
self.socket_dir = tempfile.TemporaryDirectory(prefix="webkit-flatpak-a11y-sockets-")
self.a11y_socket = tempfile.NamedTemporaryFile(dir=self.socket_dir.name, delete=False)
try:
self.dbus_proxy_process = subprocess.Popen((dbus_proxy_path, a11y_bus_address, self.a11y_socket.name, "--talk=org.a11y.Bus"), close_fds=True)
except (subprocess.CalledProcessError) as e:
_log.warning("Failed to run xdg-dbus-proxy {}. Can't forward a11y bus.".format(e))
return []
return [
# FIXME: --session-bus is only a workaround for https://github.com/flatpak/flatpak/pull/4630
"--session-bus",
"--no-a11y-bus",
"--filesystem=" + self.socket_dir.name,
"--env=AT_SPI_BUS_ADDRESS=unix:path=" + self.a11y_socket.name,
]
def run_in_sandbox(self, *args, **kwargs):
if not self.setup_builddir():
return 1
extra_env_vars = kwargs.get("env", {})
stdout = kwargs.get("stdout", sys.stdout)
extra_flatpak_args = kwargs.get("extra_flatpak_args", [])
start_sccache = kwargs.get("start_sccache", True)
skip_icc = kwargs.get("skip_icc", False)
building_local_deps = kwargs.get("building_local_deps", False)
gather_output = kwargs.get("gather_output", False)
if gather_output:
start_sccache = False
skip_icc = True
if not isinstance(args, list):
args = list(args)
sandbox_build_path = os.path.join(SANDBOX_SOURCE_ROOT, BUILD_ROOT_DIR_NAME, self.build_type)
sandbox_environment = {
"TEST_RUNNER_INJECTED_BUNDLE_FILENAME": os.path.join(sandbox_build_path, "lib/libTestRunnerInjectedBundle.so"),
"PATH": "/usr/lib/sdk/llvm14/bin:/usr/bin:/usr/lib/sdk/rust/bin/",
}
if not args:
args.append("bash")
if args:
if gather_output:
command = args[0]
elif os.path.exists(args[0]):
command = os.path.normpath(os.path.abspath(args[0]))
# Take into account the fact that the webkit source dir is remounted inside the sandbox.
args[0] = convert_webkit_source_path_to_sandbox_path(command)
if args[0] == "bash":
args.extend(['--noprofile', '--norc', '-i'])
sandbox_environment["PS1"] = f"[📦🌐🐱 $FLATPAK_ID {self.platform}@{self.build_type} \\W]\\$ "
if gather_output:
building = False
else:
building = self.is_build_jsc(args[0]) or self.is_build_webkit(args[0])
else:
building = False
flatpak_command = ["flatpak", "run",
"--user",
"--die-with-parent",
"--filesystem=host",
"--allow=devel",
"--talk-name=org.gtk.vfs",
"--talk-name=org.gtk.vfs.*"]
flatpak_a11y_args = self.setup_a11y_proxy()
flatpak_command.extend(flatpak_a11y_args)
if not gather_output and args and self.is_build_webkit(args[0]) and not self.is_branch_build():
# Ensure self.build_path exists.
try:
os.makedirs(self.build_path)
except OSError as e:
if e.errno != errno.EEXIST:
raise e
share_network_option = "--share=network"
if self.platform == 'WPE':
flatpak_command.append(share_network_option)
if not building:
flatpak_command.extend([
"--device=all",
"--device=dri",
"--share=ipc",
"--share=network",
"--socket=pulseaudio",
"--socket=session-bus",
"--socket=system-bus",
"--socket=wayland",
"--socket=x11",
"--system-talk-name=org.a11y.Bus",
"--system-talk-name=org.freedesktop.GeoClue2",
"--talk-name=org.freedesktop.Flatpak",
"--talk-name=org.freedesktop.secrets"
])
sandbox_environment.update({
"TZ": "America/Los_Angeles",
})
env_var_prefixes_to_keep = [
"G",
"CCACHE",
"EGL",
"GIGACAGE",
"GTK",
"ICECC",
"JSC",
"MESA",
"LIBGL",
"PIPEWIRE",
"NICE",
"RUST",
"SCCACHE",
"SPA",
"WAYLAND",
"WEBKIT",
"WEBKIT2",
"WPE",
]
env_var_suffixes_to_keep = [
"JSC_ARGS",
"PROCESS_CMD_PREFIX",
"WEBKIT_ARGS",
]
env_vars_to_keep = [
"CC",
"CCACHE_PREFIX",
"CFLAGS",
"CXX",
"CXXFLAGS",
"DISPLAY",
"JavaScriptCoreUseJIT",
"LDFLAGS",
"MAX_CPU_LOAD",
"Malloc",
"NUMBER_OF_PROCESSORS",
"QML2_IMPORT_PATH",
"RESULTS_SERVER_API_KEY",
"SSLKEYLOGFILE",
"XR_RUNTIME_JSON",
]
def envvar_in_suffixes_to_keep(envvar):
for env_var in env_var_suffixes_to_keep:
if envvar.endswith(env_var):
return True
return False
env_vars = os.environ
env_vars.update(extra_env_vars)
for envvar, value in env_vars.items():
var_tokens = envvar.split("_")
if var_tokens[0] in env_var_prefixes_to_keep or envvar in env_vars_to_keep or envvar_in_suffixes_to_keep(envvar) or (not os.environ.get('GST_BUILD_PATH') and var_tokens[0] == "GST"):
sandbox_environment[envvar] = value
remote_sccache_configs = set(["SCCACHE_REDIS", "SCCACHE_BUCKET", "SCCACHE_MEMCACHED",
"SCCACHE_GCS_BUCKET", "SCCACHE_AZURE_CONNECTION_STRING",
"WEBKIT_USE_SCCACHE"])
if remote_sccache_configs.intersection(set(os.environ.keys())) and start_sccache and not building_local_deps:
_log.debug("Enabling network access for the remote sccache")
flatpak_command.append(share_network_option)
sccache_environment = {}
if os.path.isfile(self.sccache_config_file) and not self.regenerate_toolchains and \
"SCCACHE_CONF" not in os.environ.keys():
sccache_environment["SCCACHE_CONF"] = convert_webkit_source_path_to_sandbox_path(self.sccache_config_file)
override_sccache_server_port = os.environ.get("WEBKIT_SCCACHE_SERVER_PORT")
if override_sccache_server_port:
_log.debug("Overriding sccache server port to %s" % override_sccache_server_port)
sccache_environment["SCCACHE_SERVER_PORT"] = override_sccache_server_port
if building:
# Spawn the sccache server in background, and avoid recursing here, using a bool keyword.
_log.debug("Pre-starting the SCCache dist server")
self.run_in_sandbox("sccache", "--start-server", env=sccache_environment, building_local_deps=building_local_deps,
extra_flatpak_args=[share_network_option], start_sccache=False)
# Forward sccache server env vars to sccache clients.
sandbox_environment.update(sccache_environment)
if self.use_icecream and not skip_icc:
_log.debug('Enabling the icecream compiler')
flatpak_command.extend([share_network_option, "--filesystem=home"])
try:
n_cores = os.environ["NUMBER_OF_PROCESSORS"]
except KeyError:
n_cores = multiprocessing.cpu_count() * 3
_log.debug('Following icecream recommendation for the number of cores to use: %d' % n_cores)
toolchain_name = os.environ.get("CC", "gcc")
default_toolchain = self.icc_version.get(toolchain_name)
try:
toolchain_override = os.environ["ICECC_VERSION_OVERRIDE"]
except KeyError:
toolchain_path = default_toolchain
else:
if not os.path.isfile(toolchain_override):
Console.error_message("%s toolchain not found. ICECC_VERSION_OVERRIDE mis-configured?", toolchain_override)
return 1
toolchain_path = convert_webkit_source_path_to_sandbox_path(toolchain_override)
if not toolchain_path:
Console.error_message("Toolchains configuration not found. Please run webkit-flatpak -r or set ICECC_VERSION_OVERRIDE to a valid host path")
return 1
if "ICECC_VERSION_APPEND" in os.environ:
extra_toolchain = os.environ["ICECC_VERSION_APPEND"]
if not os.path.isfile(extra_toolchain):
Console.error_message("%s is not a valid IceCC toolchain. ICECC_VERSION_APPEND mis-configured?", extra_toolchain)
return 1
toolchain_path += ","
toolchain_path += convert_webkit_source_path_to_sandbox_path(extra_toolchain)
sandbox_environment.update({
"CCACHE_PREFIX": "icecc",
"ICECC_TEST_SOCKET": "/run/icecc/iceccd.socket",
"ICECC_VERSION": toolchain_path,
"NUMBER_OF_PROCESSORS": n_cores,
})
# Set PKG_CONFIG_PATH in sandbox so uninstalled WebKit.pc files can be used.
pkg_config_path = os.environ.get("PKG_CONFIG_PATH")
if pkg_config_path:
pkg_config_path = "%s:%s" % (self.build_path, pkg_config_path)
else:
pkg_config_path = self.build_path
sandbox_environment["PKG_CONFIG_PATH"] = pkg_config_path
if not building_local_deps and args[0] != "sccache":
# Merge local dependencies build env vars in sandbox environment, without overriding
# previously set PATH values.
local_env = self.setup_local_deps(building)
sandbox_environment = self._merge_env_variables(sandbox_environment, local_env)
for envvar, value in sandbox_environment.items():
flatpak_command.append("--env=%s=%s" % (envvar, value))
# $WEBKIT_OUTPUTDIR is not forwarded in the build sandbox because the host build path is
# always bind-mounted to /app in the sandbox.
env_vars_to_drop = ("WEBKIT_OUTPUTDIR", "LANGUAGE")
flatpak_env = os.environ.copy()
for envvar in list(flatpak_env.keys()):
if envvar.startswith("LC_") or envvar in env_vars_to_drop:
del flatpak_env[envvar]
if self.flatpak_version >= (1, 10, 0):
flatpak_command.append("--unset-env=%s" % envvar)
# Avoid 'error: Invalid byte sequence in conversion input' after removing
# all `LANG` vars.
flatpak_env["LANG"] = "en_US.UTF-8"
keep_signals = args[0] != "gdb"
if not keep_signals:
module_path = os.path.join(self.build_path, "lib", "libsigaction-disabler.so")
# Enable module in bwrap child processes.
extra_flatpak_args.append("--env=WEBKIT_FLATPAK_LD_PRELOAD=%s" % module_path)
# Enable module in `flatpak run`.
flatpak_env["LD_PRELOAD"] = module_path
flatpak_command += extra_flatpak_args + ['--command=%s' % args[0], "org.webkit.Sdk"] + args[1:]
flatpak_env.update({
"FLATPAK_BWRAP": os.path.join(SCRIPT_DIR, "webkit-bwrap"),
"WEBKIT_BUILD_DIR_BIND_MOUNT": "%s:%s" % (sandbox_build_path, self.build_path),
"WEBKIT_FLATPAK_USER_DIR": os.environ["FLATPAK_USER_DIR"],
})
display = os.environ.get("DISPLAY")
if display:
flatpak_env["WEBKIT_FLATPAK_DISPLAY"] = display
# FIXME: Remove duplicate values from the flatpak command.
command = flatpak_command
if gather_output:
return run_sanitized(command, gather_output=True, ignore_stderr=False, env=flatpak_env)
try:
return self.execute_command(command, stdout=stdout, env=flatpak_env, keep_signals=keep_signals)
except KeyboardInterrupt:
return 0
return 0
def main(self):
if self.check_available:
return 0
if not self.clean_args():
return 1
if self.show_version:
print(self.sdk_repo.version("org.webkit.Sdk"))
return 0
if self.update:
flatpak_wrapper = FlatpakObject(True)
version_before_update = flatpak_wrapper.version("org.webkit.Sdk")
flatpak_wrapper.flatpak_update()
regenerate_toolchains = (flatpak_wrapper.version("org.webkit.Sdk") != version_before_update) or not self.check_toolchains_generated()
# If we have an out-of-date package, simply remove the entire flatpak directory and start over.
for package in self._get_dependency_packages():
if package.name.startswith("org.webkit") \
and self.repos.is_package_installed(package.name) \
and not self.repos.is_package_installed(package.name, branch=SDK_BRANCH):
# Cache sccache auth token before removing UserFlatpak.
self.acquire_sccache_auth_token_from_config_file()
Console.message("New SDK version available, removing local UserFlatpak directory before switching to new version")
shutil.rmtree(self.flatpak_build_path)
Console.message("Forcing next WebKit build to re-run CMake")
for platform in ('GTK', 'WPE'):
for build_type in ('Release', 'Debug'):
cache_path = os.path.join(get_build_dir(platform, build_type), 'CMakeCache.txt')
if os.path.isfile(cache_path):
Console.message("Removing %s", cache_path)
os.remove(cache_path)
self._reset_repository()
break
for package in self._get_dependency_packages():
if not self.repos.is_package_installed(package.name):
package.install()
regenerate_toolchains = True
print("SDK version: {}".format(self.sdk_repo.version("org.webkit.Sdk")))
else:
regenerate_toolchains = self.regenerate_toolchains
result = self.setup_dev_env()
if regenerate_toolchains:
# Toolchains used to be stored in WebKitBuild/Toolchains. Remove this path if found, to save
# up disk space.
old_toolchains_path = os.path.join(DEFAULT_BUILD_ROOT, "Toolchains")
if os.path.isdir(old_toolchains_path):
Console.message("Purging obsolete toolchains")
shutil.rmtree(old_toolchains_path)
if not os.path.isdir(self.toolchains_directory):
os.makedirs(self.toolchains_directory)
Console.message("Updating icecc/sccache standalone toolchain archives")
self.icc_version = {}
gcc_archive, toolchains = self.pack_toolchain(("gcc", "g++"), {"/usr/bin/c++": "g++",
"/usr/bin/cc": "gcc"})
clang_archive, clang_toolchains = self.pack_toolchain(("clang", "clang++"), {"/usr/bin/clang++": "clang++",
"/usr/bin/clang": "clang"})
toolchains.extend(clang_toolchains)
if len(toolchains) > 1:
self.save_config(toolchains)
self.purge_unused_toolchains((gcc_archive, clang_archive))
else:
Console.error_message("Error generating icecc/sccache standalone toolchain archives")
return result
def run(self):
try:
return self.main()
except subprocess.CalledProcessError as error:
Console.error_message("The following command returned a non-zero exit status: %s\n"
"Output: %s", ' '.join(error.cmd), error.output)
return error.returncode
return 0
def has_environment(self):
return os.path.exists(self.flatpak_build_path)
def acquire_sccache_auth_token_from_config_file(self):
if os.path.isfile(self.sccache_config_file) and not self.sccache_token:
Console.message("Reusing sccache auth token from old configuration file")
with open(self.sccache_config_file) as config:
sccache_config = toml.load(config)
self.sccache_token = sccache_config['dist']['auth']['token']
def save_config(self, toolchains):
with open(self.config_file, 'w') as config:
json_config = {'icecc_version': self.icc_version}
json.dump(json_config, config)
self.acquire_sccache_auth_token_from_config_file()
if not self.sccache_token:
Console.message("No authentication token provided. Re-run this with the -t option if an sccache token was provided to you. Skipping sccache configuration for now.")
return
with open(self.sccache_config_file, 'w') as config:
sccache_config = {'dist': {'scheduler_url': self.sccache_scheduler,
'auth': {'type': 'token',
'token': self.sccache_token},
'toolchains': toolchains}}
toml.dump(sccache_config, config)
Console.message("Created %s sccache config file. It will automatically be used when building WebKit", self.sccache_config_file)
def purge_unused_toolchains(self, allow_list):
for filename in os.listdir(self.toolchains_directory):
if filename not in allow_list:
_log.debug("Removing unused toolchain: %s", filename)
os.remove(os.path.join(self.toolchains_directory, filename))
def check_toolchains_generated(self):
found_toolchains = 0
if os.path.isfile(self.config_file):
with open(self.config_file, 'r') as config_fd:
config = json.load(config_fd)
if 'icecc_version' in config:
for compiler in config['icecc_version']:
if os.path.isfile(convert_sandbox_path_to_webkit_source_path(config['icecc_version'][compiler])):
found_toolchains += 1
return found_toolchains > 1
def pack_toolchain(self, compilers, path_mapping):
compiler_mapping = {}
for compiler in compilers:
compiler_mapping[compiler] = self.run_in_sandbox("/usr/bin/which", compiler, gather_output=True)
with tempfile.NamedTemporaryFile() as tmpfile:
command = ['icecc', '--build-native']
command.extend(compiler_mapping.values())
retcode = self.run_in_sandbox(*command, stdout=tmpfile, cwd=WEBKIT_SOURCE_DIR, skip_icc=True)
if retcode != 0:
Console.error_message('Flatpak command "%s" failed with return code %s', " ".join(command), retcode)
return []
tmpfile.flush()
tmpfile.seek(0)
icc_version_filename, = re.findall(br'.*creating (.*)', tmpfile.read())
relative_filename = "webkit-sdk-{name}-{filename}".format(name=compilers[0], filename=icc_version_filename.decode())
archive_filename = os.path.join(self.toolchains_directory, relative_filename)
os.rename(icc_version_filename, archive_filename)
archive_sandbox_path = convert_webkit_source_path_to_sandbox_path(archive_filename)
self.icc_version[compilers[0]] = archive_sandbox_path
Console.message("Created %s self-contained toolchain archive", archive_filename)
sccache_toolchains = []
for (compiler_executable, archive_compiler_executable) in path_mapping.items():
item = {'type': 'path_override',
'compiler_executable': compiler_executable,
'archive': archive_sandbox_path,
'archive_compiler_executable': compiler_mapping[archive_compiler_executable]}
sccache_toolchains.append(item)
return (relative_filename, sccache_toolchains)
def check_installed_packages(self):
for package in self._get_dependency_packages():
if package.name.startswith("org.webkit") and not package.is_installed(SDK_BRANCH):
Console.error_message("Flatpak package %s not installed. Please update your SDK: Tools/Scripts/update-webkit-flatpak", package)
return False
else:
return True
def setup_dev_env(self):
if not os.path.exists(os.path.join(self.flatpak_build_path, "runtime", "org.webkit.Sdk")) or self.update:
self.install_all()
if not self.update and not self.check_installed_packages():
return 1
if self.gdb or self.gdb_stack_trace:
return self.run_gdb()
elif self.user_command:
program = self.user_command[0]
if self.is_build_webkit(program) and self.cmakeargs:
self.user_command.append("--cmakeargs=%s" % self.cmakeargs)
return self.run_in_sandbox(*self.user_command + self.extra_command_args)
elif not self.update and not self.build_local_deps and not self.regenerate_toolchains:
return self.run_in_sandbox()
return 0
def _get_dependency_packages(self):
arch = platform.machine()
self.runtime = FlatpakPackage("org.webkit.Platform", SDK_BRANCH,
self.sdk_repo, arch)
self.sdk = FlatpakPackage("org.webkit.Sdk", SDK_BRANCH,
self.sdk_repo, arch)
packages = [self.runtime, self.sdk]
packages.append(FlatpakPackage('org.webkit.Sdk.Debug', SDK_BRANCH,
self.sdk_repo, arch))
packages.append(FlatpakPackage("org.freedesktop.Sdk.Extension.llvm14", SDK_BRANCH,
self.flathub_repo, arch))
packages.append(FlatpakPackage("org.freedesktop.Platform.GL.default", SDK_BRANCH,
self.flathub_repo, arch))
return packages
def install_all(self):
if os.path.exists(os.path.join(self.flatpak_build_path, "runtime", "org.webkit.Sdk")):
return
Console.message("Installing %s dependencies in %s", self.build_type, self.flatpak_build_path)
for package in self._get_dependency_packages():
if not package.is_installed(SDK_BRANCH):
package.install()
def run_gdb(self):
with disable_signals():
try:
subprocess.check_output(['which', 'coredumpctl'])
except subprocess.CalledProcessError as e:
Console.message("'coredumpctl' not present on the system, can't run. (%s)\n", e)
return e.returncode
# We need access to the host from the sandbox to run.
with tempfile.NamedTemporaryFile() as coredump:
with tempfile.NamedTemporaryFile() as stderr:
if self.gdb_stack_trace:
cmd = ["coredumpctl", "--since=%s" % self.gdb_stack_trace, "dump"]
else:
cmd = ["coredumpctl", "dump"] + shlex.split(self.coredumpctl_matches)
result = self.execute_command(cmd, stdout=coredump, stderr=stderr)
if result != 0:
Console.error_message("coredumpctl failed")
with open(stderr.name, 'r') as stderrf:
stderr = stderrf.read()
Console.error_message(stderr)
return result
with open(stderr.name, 'r') as stderrf:
stderr = stderrf.read()
executable, = re.findall(".*Executable: (.*)", stderr)
if self.gdb:
bargs = ["gdb", executable, "/run/host/%s" % coredump.name]
if type(self.gdb) != bool:
bargs.extend(shlex.split(self.gdb))
elif self.gdb_stack_trace:
bargs = ["gdb", '-ex', "thread apply all backtrace", '--batch', executable, "/run/host/%s" % coredump.name]
return self.run_in_sandbox(*bargs)
def is_sandboxed():
return os.path.exists("/.flatpak-info")
def run_in_sandbox_if_available(args):
if os.environ.get('WEBKIT_JHBUILD', '0') == '1':
return None
os.environ["FLATPAK_USER_DIR"] = os.environ.get("WEBKIT_FLATPAK_USER_DIR", FLATPAK_USER_DIR_PATH)
if not os.path.isdir(os.environ["FLATPAK_USER_DIR"]):
return None
if is_sandboxed():
return None
if not check_flatpak(verbose=False):
return None
# Filter out flatpakutils args for the app.
runner_args = []
app_args = []
opt_prefix = "--flatpak-"
for arg in args:
if arg.startswith(opt_prefix):
runner_args.append("--%s" % arg[len(opt_prefix):])
else:
runner_args.append(arg)
app_args.append(arg)
flatpak_runner = WebkitFlatpak.load_from_args(runner_args, add_help=False)
if not flatpak_runner.clean_args():
return None
if not flatpak_runner.has_environment():
return None
sys.exit(flatpak_runner.run_in_sandbox(*app_args))