blob: 6a9269f29b153525d0936228fdff372531bc0d81 [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 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
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"
scriptdir = os.path.abspath(os.path.dirname(__file__))
_log = logging.getLogger(__name__)
FLATPAK_USER_DIR_PATH = os.path.realpath(os.path.join(scriptdir, "../../WebKitBuild", "UserFlatpak"))
DEFAULT_SCCACHE_SCHEDULER='https://sccache.igalia.com'
# 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"
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):
""" 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 = dict(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
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
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)
ignore_stderr = kwargs.get("ignore_stderr", 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)
_log.debug("Executing %s" % ' '.join(command))
return run_sanitized(command, gather_output=gather_output, ignore_stderr=ignore_stderr)
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 ""
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()
def update(self):
if not self.is_installed(self.branch):
return self.install()
comment = "Updating %s" % self.name
self.flatpak("update", self.name, self.branch, comment=comment)
@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)
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("-bgst", "--build-gst", dest="build_gst",
action="store_true",
help="Force rebuilding gst-build, repository path is defined by the `GST_BUILD_PATH` environment variable.")
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\")")
_, self.args = parser.parse_known_args(args=args, namespace=self)
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.source_root = os.path.normpath(os.path.abspath(os.path.join(scriptdir, '../../')))
# Where the source folder is mounted inside the sandbox.
self.sandbox_source_root = "/app/webkit"
self.base_build_dir = 'WebKitBuild'
self.build_gst = 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
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
# 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.
self.default_build_root = os.path.join(self.source_root, self.base_build_dir)
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')
build_root = os.environ.get("WEBKIT_OUTPUTDIR", self.source_root)
self.build_root = os.path.join(build_root, self.base_build_dir)
self.build_path = os.path.join(self.build_root, 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 = "https://software.igalia.com/webkit-sdk-repo/"
repo_file = "https://software.igalia.com/flatpak-refs/webkit-sdk.flatpakrepo"
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_gstbuild(self, building):
gst_dir = os.environ.get('GST_BUILD_PATH')
if not gst_dir:
if building:
_log.debug("$GST_BUILD_PATH environment variable not set. Skipping gst-build\n")
return {}
if not os.path.exists(os.path.join(gst_dir, 'gst-env.py')):
raise RuntimeError('GST_BUILD_PATH set to %s but it doesn\'t seem to be a valid `gst-build` checkout.' % gst_dir)
gst_builddir = os.path.join(self.source_root, self.base_build_dir, 'gst-build')
if not os.path.exists(os.path.join(self.default_build_root, 'gst-build', 'build.ninja')):
if not building:
raise RuntimeError('Trying to enter gst-build env from %s but it is not built, make sure to rebuild webkit.' % gst_dir)
args = ['meson', ]
extra_args = os.environ.get('GST_BUILD_ARGS', '')
args.extend(shlex.split(extra_args) + [gst_dir, gst_builddir])
Console.message("Running %s ", ' '.join(args))
self.run_in_sandbox(*args, building_gst=True, start_sccache=False)
if building:
Console.message("Building `gst-build` %s ", gst_dir)
if self.run_in_sandbox('ninja', '-C', gst_builddir, building_gst=True, start_sccache=False) != 0:
raise RuntimeError('Error while building gst-build.')
command = [os.path.join(gst_dir, 'gst-env.py'), '--builddir', gst_builddir, '--srcdir', gst_dir, "--only-environment"]
gst_env = run_sanitized(command, gather_output=True)
allowlist = ("LD_LIBRARY_PATH", "PATH", "PKG_CONFIG_PATH")
nopathlist = ("GST_DEBUG", "GST_VERSION", "GST_ENV")
env = {}
for line in [line for line in gst_env.splitlines() if not line.startswith("export")]:
tokens = line.split("=")
var_name, contents = tokens[0], "=".join(tokens[1:])
if not var_name.startswith("GST_") and var_name not in allowlist:
continue
if var_name not in nopathlist:
new_contents = ':'.join([self.host_path_to_sandbox_path(p) for p in contents.split(":")])
else:
new_contents = contents.replace("'", "")
env[var_name] = new_contents
return env
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 host_path_to_sandbox_path(self, host_path):
# For now this supports only files in the WebKit path
return host_path.replace(self.source_root, self.sandbox_source_root)
def run_in_sandbox(self, *args, **kwargs):
if not self.setup_builddir():
return 1
cwd = kwargs.get("cwd", None)
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_gst = kwargs.get("building_gst", False)
gather_output = kwargs.get("gather_output", False)
if gather_output:
start_sccache = False
skip_icc = True
building_gst = False
if not isinstance(args, list):
args = list(args)
sandbox_build_path = os.path.join(self.sandbox_source_root, self.base_build_dir, self.build_type)
sandbox_environment = {
"TEST_RUNNER_INJECTED_BUNDLE_FILENAME": os.path.join(sandbox_build_path, "lib/libTestRunnerInjectedBundle.so"),
"PATH": "/usr/lib/sdk/llvm12/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] = command.replace(self.source_root, self.sandbox_source_root)
if args[0] == "bash":
args.extend(['--noprofile', '--norc', '-i'])
sandbox_environment["PS1"] = "[📦🌐🐱 $FLATPAK_ID \\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.a11y.Bus",
"--talk-name=org.gtk.vfs",
"--talk-name=org.gtk.vfs.*"]
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": "PST8PDT",
})
env_var_prefixes_to_keep = [
"G",
"CCACHE",
"EGL",
"GIGACAGE",
"GTK",
"ICECC",
"JSC",
"MESA",
"LIBGL",
"PIPEWIRE",
"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_gst:
_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"] = self.host_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_gst=building_gst,
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 = self.host_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 += self.host_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_gst and args[0] != "sccache":
# Merge gst-build env vars in sandbox environment, without overriding previously set PATH values.
gst_env = self.setup_gstbuild(building)
for var_name in list(gst_env.keys()):
if var_name not in sandbox_environment:
sandbox_environment[var_name] = gst_env[var_name]
else:
contents = gst_env[var_name]
if var_name.endswith('PATH'):
sandbox_environment[var_name] = "%s:%s" % (sandbox_environment[var_name], contents)
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(scriptdir, "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=True, 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:
repo = self.sdk_repo
version_before_update = repo.version("org.webkit.Sdk")
repo.flatpak("update", comment="Updating Flatpak %s environment" % self.build_type)
regenerate_toolchains = (repo.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(self.build_root, 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
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(self.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(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=self.source_root, 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 = self.host_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)
elif not self.update and not self.build_gst 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.llvm12", 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))