1
0
Fork 0
mirror of https://github.com/LadybirdBrowser/ladybird.git synced 2025-06-07 21:17:07 +09:00
ladybird/Meta/ladybird.py

521 lines
18 KiB
Python
Executable file

#!/usr/bin/env python3
# Copyright (c) 2025, ayeteadoe <ayeteadoe@gmail.com>
# Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
#
# SPDX-License-Identifier: BSD-2-Clause
import argparse
import importlib.util
import multiprocessing
import os
import platform
import re
import shutil
import subprocess
import sys
import types
from enum import IntEnum
from pathlib import Path
def import_module(module_path: Path) -> types.ModuleType:
spec = importlib.util.spec_from_file_location(module_path.stem, module_path)
if not spec or not spec.loader:
raise ModuleNotFoundError(f"Could not find module {module_path}")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
META_PATH = Path(__file__).parent
TOOLCHAIN_PATH = META_PATH.parent / "Toolchain"
BuildVcpkg = import_module(TOOLCHAIN_PATH / "BuildVcpkg.py")
class HostArchitecture(IntEnum):
Unsupported = 0
x86_64 = 1
AArch64 = 2
class HostSystem(IntEnum):
Unsupported = 0
Linux = 1
macOS = 2
Windows = 3
class Platform:
def __init__(self):
self.system = platform.system()
if self.system == "Windows":
self.host_system = HostSystem.Windows
elif self.system == "Darwin":
self.host_system = HostSystem.macOS
elif self.system == "Linux":
self.host_system = HostSystem.Linux
else:
self.host_system = HostSystem.Unsupported
self.architecture = platform.machine().lower()
if self.architecture in ("x86_64", "amd64"):
self.host_architecture = HostArchitecture.x86_64
elif self.architecture in ("aarch64", "arm64"):
self.host_architecture = HostArchitecture.AArch64
else:
self.host_architecture = HostArchitecture.Unsupported
def main():
platform = Platform()
if platform.host_system == HostSystem.Unsupported:
print(f"Unsupported host system {platform.system}", file=sys.stderr)
sys.exit(1)
if platform.host_architecture == HostArchitecture.Unsupported:
print(f"Unsupported host architecture {platform.architecture}", file=sys.stderr)
sys.exit(1)
parser = argparse.ArgumentParser(description="Ladybird")
subparsers = parser.add_subparsers(dest="command")
preset_parser = argparse.ArgumentParser(add_help=False)
preset_parser.add_argument(
"--preset",
required=False,
default=os.environ.get(
"BUILD_PRESET", "windows_dev_ninja" if platform.host_system == HostSystem.Windows else "default"
),
)
# FIXME: Validate that the cc/cxx combination is compatible (e.g. don't allow CC=gcc and CXX=clang++)
# FIXME: Migrate find_compiler.sh for more explicit compiler validation
compiler_parser = argparse.ArgumentParser(add_help=False)
compiler_parser.add_argument(
"--cc",
required=False,
default=os.environ.get("CC", "clang-cl" if platform.host_system == HostSystem.Windows else "cc"),
)
compiler_parser.add_argument(
"--cxx",
required=False,
default=os.environ.get("CXX", "clang-cl" if platform.host_system == HostSystem.Windows else "c++"),
)
target_parser = argparse.ArgumentParser(add_help=False)
target_parser.add_argument("target", nargs=argparse.OPTIONAL, default="Ladybird")
build_parser = subparsers.add_parser(
"build", help="Compiles the target binaries", parents=[preset_parser, compiler_parser, target_parser]
)
build_parser.add_argument(
"args", nargs=argparse.REMAINDER, help="Additional arguments passed through to the build system"
)
test_parser = subparsers.add_parser(
"test", help="Runs the unit tests on the build host", parents=[preset_parser, compiler_parser]
)
test_parser.add_argument(
"--pattern", required=False, help="Limits the tests that are ran to those that match the regex pattern"
)
run_parser = subparsers.add_parser(
"run", help="Runs the application on the build host", parents=[preset_parser, compiler_parser, target_parser]
)
run_parser.add_argument(
"args", nargs=argparse.REMAINDER, help="Additional arguments passed through to the application"
)
debug_parser = subparsers.add_parser(
"debug",
help="Launches the application on the build host in a gdb or lldb session",
parents=[preset_parser, target_parser],
)
debug_parser.add_argument(
"--debugger", required=False, default="gdb" if platform.host_system == HostSystem.Linux else "lldb"
)
debug_parser.add_argument(
"-cmd", action="append", required=False, default=[], help="Additional commands passed through to the debugger"
)
subparsers.add_parser(
"install", help="Installs the target binary", parents=[preset_parser, compiler_parser, target_parser]
)
subparsers.add_parser(
"vcpkg", help="Ensure that dependencies are available", parents=[preset_parser, compiler_parser]
)
subparsers.add_parser("clean", help="Cleans the build environment", parents=[preset_parser, compiler_parser])
rebuild_parser = subparsers.add_parser(
"rebuild",
help="Cleans the build environment and compiles the target binaries",
parents=[preset_parser, compiler_parser, target_parser],
)
rebuild_parser.add_argument(
"args", nargs=argparse.REMAINDER, help="Additional arguments passed through to the build system"
)
addr2line_parser = subparsers.add_parser(
"addr2line",
help="Resolves the addresses in the target binary to a file:line",
parents=[preset_parser, compiler_parser, target_parser],
)
addr2line_parser.add_argument(
"--program",
required=False,
default=(
"llvm-symbolizer"
if platform.host_system == HostSystem.Windows
else "addr2line" if platform.host_system == HostSystem.Linux else "atos"
),
)
addr2line_parser.add_argument("addresses", nargs=argparse.REMAINDER)
args = parser.parse_args()
if not args.command:
parser.print_help()
sys.exit(1)
if platform.host_system != HostSystem.Windows and os.geteuid() == 0:
print("Do not run ladybird.py as root, your Build directory will become root-owned", file=sys.stderr)
sys.exit(1)
elif platform.host_system == HostSystem.Windows and "VCINSTALLDIR" not in os.environ:
print("ladybird.py must be run from a Visual Studio enabled environment", file=sys.stderr)
sys.exit(1)
if args.target == "ladybird":
args.target = "Ladybird"
if args.command == "build":
build_dir = configure_main(platform, args.preset, args.cc, args.cxx)
build_main(build_dir, args.target, args.args)
elif args.command == "test":
build_dir = configure_main(platform, args.preset, args.cc, args.cxx)
build_main(build_dir)
test_main(build_dir, args.preset, args.pattern)
elif args.command == "run":
if args.preset == "Sanitizer":
# FIXME: Find some way to centralize these b/w CMakePresets.json, CI files, Documentation and here.
os.environ["ASAN_OPTIONS"] = os.environ.get(
"ASAN_OPTIONS",
"strict_string_checks=1:check_initialization_order=1:strict_init_order=1:"
"detect_stack_use_after_return=1:allocator_may_return_null=1",
)
os.environ["UBSAN_OPTIONS"] = os.environ.get(
"UBSAN_OPTIONS", "print_stacktrace=1:print_summary=1:halt_on_error=1"
)
build_dir = configure_main(platform, args.preset, args.cc, args.cxx)
build_main(build_dir, args.target)
run_main(platform.host_system, build_dir, args.target, args.args)
elif args.command == "debug":
build_dir = configure_main(platform, args.preset, args.cc, args.cxx)
build_main(build_dir, args.target, args.args)
debug_main(platform.host_system, build_dir, args.target, args.debugger, args.cmd)
elif args.command == "install":
build_dir = configure_main(platform, args.preset, args.cc, args.cxx)
build_main(build_dir, args.target, args.args)
build_main(build_dir, "install", args.args)
elif args.command == "vcpkg":
configure_build_env(args.preset, args.cc, args.cxx)
BuildVcpkg.build_vcpkg()
elif args.command == "clean":
clean_main(args.preset, args.cc, args.cxx)
elif args.command == "rebuild":
clean_main(args.preset, args.cc, args.cxx)
build_dir = configure_main(platform, args.preset, args.cc, args.cxx)
build_main(build_dir, args.target, args.args)
elif args.command == "addr2line":
build_dir = configure_main(platform, args.preset, args.cc, args.cxx)
build_main(build_dir, args.target)
addr2line_main(build_dir, args.target, args.program, args.addresses)
def configure_main(platform: Platform, preset: str, cc: str, cxx: str) -> Path:
ladybird_source_dir, build_preset_dir, build_env_cmake_args = configure_build_env(preset, cc, cxx)
BuildVcpkg.build_vcpkg()
if build_preset_dir.joinpath("build.ninja").exists() or build_preset_dir.joinpath("ladybird.sln").exists():
return build_preset_dir
cmake_args = []
host_system = platform.host_system
if host_system == HostSystem.Linux and platform.host_architecture == HostArchitecture.AArch64:
cmake_args.extend(configure_skia_jemalloc())
validate_cmake_version()
config_args = [
"cmake",
"--preset",
preset,
"-S",
ladybird_source_dir,
"-B",
build_preset_dir,
]
config_args.extend(build_env_cmake_args)
config_args.extend(cmake_args)
# FIXME: Improve error reporting for vcpkg install failures
# https://github.com/LadybirdBrowser/ladybird/blob/master/Documentation/BuildInstructionsLadybird.md#unable-to-find-a-build-program-corresponding-to-ninja
try:
subprocess.check_call(config_args)
except subprocess.CalledProcessError as e:
print_process_stderr(e, "Unable to configure ladybird project")
sys.exit(1)
return build_preset_dir
def configure_skia_jemalloc() -> list[str]:
# NOTE: The resource module is only available on Unix, see the "Availability" section at
# https://docs.python.org/3/library/resource.html. Given Windows never calls this function, we import locally
# instead.
import resource
page_size = resource.getpagesize()
gn = shutil.which("gn") or None
# https://github.com/LadybirdBrowser/ladybird/issues/261
if page_size != 4096 and gn is None:
print("GN not found! Please build GN from source and put it in $PATH", file=sys.stderr)
sys.exit(1)
cmake_args = []
pkg_config = shutil.which("pkg-config") or None
if pkg_config:
cmake_args.append(f"-DPKG_CONFIG_EXECUTABLE={pkg_config}")
user_vars_cmake_module = Path("Meta", "CMake", "vcpkg", "user-variables.cmake")
user_vars_cmake_module.parent.mkdir(parents=True, exist_ok=True)
with open(user_vars_cmake_module, "w") as f:
f.writelines(
[
f"set(PKGCONFIG {pkg_config})",
f"set(GN {gn})",
"",
]
)
return cmake_args
def configure_build_env(preset: str, cc: str, cxx: str) -> tuple[Path, Path, list[str]]:
cmake_args = [
f"-DCMAKE_C_COMPILER={cc}",
f"-DCMAKE_CXX_COMPILER={cxx}",
]
ladybird_source_dir = ensure_ladybird_source_dir()
build_root_dir = ladybird_source_dir / "Build"
known_presets = {
"default": build_root_dir / "release",
"windows_ci_ninja": build_root_dir / "release",
"windows_dev_ninja": build_root_dir / "debug",
"windows_dev_msbuild": build_root_dir / "debug",
"Debug": build_root_dir / "debug",
"Sanitizer": build_root_dir / "sanitizers",
"Distribution": build_root_dir / "distribution",
}
build_preset_dir = known_presets.get(preset, None)
if not build_preset_dir:
print(f'Unknown build preset "{preset}"', file=sys.stderr)
sys.exit(1)
vcpkg_root = str(build_root_dir / "vcpkg")
os.environ["VCPKG_ROOT"] = vcpkg_root
os.environ["PATH"] += os.pathsep + str(ladybird_source_dir.joinpath("Toolchain", "Local", "cmake", "bin"))
os.environ["PATH"] += os.pathsep + str(vcpkg_root)
return ladybird_source_dir, build_preset_dir, cmake_args
def validate_cmake_version():
# FIXME: This 3.25+ CMake version check may not be needed anymore due to vcpkg downloading a newer version
cmake_install_message = "Please install CMake version 3.25 or newer."
try:
cmake_version_output = subprocess.check_output(["cmake", "--version"], text=True).strip()
version_match = re.search(r"version\s+(\d+)\.(\d+)\.(\d+)?", cmake_version_output)
if version_match:
major = int(version_match.group(1))
minor = int(version_match.group(2))
patch = int(version_match.group(3))
if major < 3 or (major == 3 and minor < 25):
print(f"CMake version {major}.{minor}.{patch} is too old. {cmake_install_message}", file=sys.stderr)
sys.exit(1)
else:
print(f"Unable to determine CMake version. {cmake_install_message}", file=sys.stderr)
sys.exit(1)
except subprocess.CalledProcessError as e:
print_process_stderr(e, f"CMake not found. {cmake_install_message}\n")
sys.exit(1)
def ensure_ladybird_source_dir() -> Path:
ladybird_source_dir = os.environ.get("LADYBIRD_SOURCE_DIR", None)
ladybird_source_dir = Path(ladybird_source_dir) if ladybird_source_dir else None
if not ladybird_source_dir or not ladybird_source_dir.is_dir():
try:
top_dir = subprocess.check_output(["git", "rev-parse", "--show-toplevel"], text=True).strip()
ladybird_source_dir = Path(top_dir)
os.environ["LADYBIRD_SOURCE_DIR"] = str(ladybird_source_dir)
except subprocess.CalledProcessError as e:
print_process_stderr(e, "Unable to determine LADYBIRD_SOURCE_DIR:")
sys.exit(1)
return ladybird_source_dir
def build_main(build_dir: Path, target: str | None = None, args: list[str] = []):
build_args = [
"cmake",
"--build",
str(build_dir),
"--parallel",
os.environ.get("MAKEJOBS", str(multiprocessing.cpu_count())),
]
if target:
build_args.extend(["--target", target])
if args:
build_args.append("--")
build_args.extend(args)
try:
subprocess.check_call(build_args)
except subprocess.CalledProcessError as e:
print_process_stderr(e, f"Unable to build Ladybird {f'target {target}' if target else 'project'}")
sys.exit(1)
def test_main(build_dir: Path, preset: str, pattern: str | None):
test_args = [
"ctest",
"--preset",
preset,
"--output-on-failure",
"--test-dir",
str(build_dir),
]
if pattern:
test_args.extend(["-R", pattern])
try:
subprocess.check_call(test_args)
except subprocess.CalledProcessError as e:
print_process_stderr(e, f"Unable to test Ladybird {f'pattern {pattern}' if pattern else 'project'}")
sys.exit(1)
def run_main(host_system: HostSystem, build_dir: Path, target: str, args: list[str]):
run_args = []
if host_system == HostSystem.macOS and target in (
"headless-browser",
"ImageDecoder",
"Ladybird",
"RequestServer",
"WebContent",
"WebDriver",
"WebWorker",
):
run_args.append(str(build_dir.joinpath("bin", "Ladybird.app", "Contents", "MacOS", target)))
else:
run_args.append(str(build_dir.joinpath("bin", target)))
run_args.extend(args)
try:
# FIXME: For Windows, set the working directory so DLLs are found
subprocess.check_call(run_args)
except subprocess.CalledProcessError as e:
print_process_stderr(e, f'Unable to run ladybird target "{target}"')
sys.exit(1)
def debug_main(host_system: HostSystem, build_dir: Path, target: str, debugger: str, debugger_commands: list[str]):
debuggers = ["gdb", "lldb"]
if debugger not in debuggers or not shutil.which(debugger):
print("Please install gdb or lldb!", file=sys.stderr)
sys.exit(1)
gdb_args = [debugger]
for cmd in debugger_commands:
gdb_args.extend(["-ex" if debugger == "gdb" else "-o", cmd])
if target == "Ladybird" and host_system == HostSystem.macOS:
gdb_args.append(str(build_dir.joinpath("bin", "Ladybird.app")))
else:
gdb_args.append(str(build_dir.joinpath("bin", target)))
try:
# FIXME: For Windows, set the working directory so DLLs are found
subprocess.check_call(gdb_args)
except subprocess.CalledProcessError as e:
print_process_stderr(e, f'Unable to run ladybird target "{target}" with "{debugger}" debugger')
sys.exit(1)
def clean_main(preset: str, cc: str, cxx: str):
ladybird_source_dir, build_preset_dir, _ = configure_build_env(preset, cc, cxx)
shutil.rmtree(str(build_preset_dir), ignore_errors=True)
user_vars_cmake_module = ladybird_source_dir.joinpath("Meta", "CMake", "vcpkg", "user-variables.cmake")
user_vars_cmake_module.unlink(missing_ok=True)
def addr2line_main(build_dir, target: str, program: str, addresses: list[str]):
if not shutil.which(program):
print(f"Please install {program}!", file=sys.stderr)
sys.exit(1)
binary_file_path = None
for root, _, files in os.walk(build_dir):
if target in files:
candidate = Path(root) / target
if os.access(candidate, os.X_OK):
binary_file_path = str(candidate)
if not binary_file_path:
print(f'Unable to find binary target "{target}" in build directory "{build_dir}"', file=sys.stderr)
sys.exit(1)
addr2line_args = [
program,
"-o" if program == "atos" else "-e",
binary_file_path,
]
addr2line_args.extend(addresses)
try:
subprocess.check_call(addr2line_args)
except subprocess.CalledProcessError as e:
print_process_stderr(e, f'Unable to find lines with "{program}" for binary target "{target}"')
sys.exit(1)
def print_process_stderr(exception: subprocess.CalledProcessError, message: str):
details = f": {exception.stderr}" if exception.stderr else ""
print(f"{message}{details}", file=sys.stderr)
if __name__ == "__main__":
main()