1
0
Fork 0
mirror of https://github.com/LadybirdBrowser/ladybird.git synced 2025-06-08 05:27:14 +09:00
ladybird/Meta/ladybird.py
Timothy Flynn aa0fcc67b3 Meta: Tentatively support BSD distributions in ladybird.py
Support was unknowingly dropped when porting ladybird.sh to ladybird.py.
This tentatively restores this support, but is untested on a BSD system.
2025-05-29 16:24:17 -04:00

416 lines
15 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 multiprocessing
import os
import platform
import re
import shutil
import sys
from pathlib import Path
from typing import Optional
sys.path.append(str(Path(__file__).resolve().parent.parent))
from Meta.find_compiler import pick_host_compiler
from Meta.host_platform import HostArchitecture
from Meta.host_platform import HostSystem
from Meta.host_platform import Platform
from Meta.utils import run_command
from Toolchain.BuildVcpkg import build_vcpkg
def main():
platform = Platform()
(default_cc, default_cxx) = platform.default_compiler()
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"
),
)
compiler_parser = argparse.ArgumentParser(add_help=False)
compiler_parser.add_argument("--cc", required=False, default=default_cc)
compiler_parser.add_argument("--cxx", required=False, default=default_cxx)
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=platform.default_debugger())
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=platform.default_symbolizer())
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"
(cc, cxx) = pick_host_compiler(platform, args.cc, args.cxx)
if args.command == "build":
build_dir = configure_main(platform, args.preset, cc, cxx)
build_main(build_dir, args.target, args.args)
elif args.command == "test":
build_dir = configure_main(platform, args.preset, cc, 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, cc, 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, cc, 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, cc, 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, cc, cxx)
build_vcpkg()
elif args.command == "clean":
clean_main(args.preset, cc, cxx)
elif args.command == "rebuild":
clean_main(args.preset, cc, cxx)
build_dir = configure_main(platform, args.preset, cc, cxx)
build_main(build_dir, args.target, args.args)
elif args.command == "addr2line":
build_dir = configure_main(platform, args.preset, cc, 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)
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
run_command(config_args, exit_on_failure=True)
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."
cmake_version_output = run_command(["cmake", "--version"], return_output=True, exit_on_failure=True)
assert cmake_version_output
version_match = re.search(r"version\s+(\d+)\.(\d+)\.(\d+)?", cmake_version_output)
if not version_match:
print(f"Unable to determine CMake version. {cmake_install_message}", file=sys.stderr)
sys.exit(1)
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)
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():
root_dir = run_command(["git", "rev-parse", "--show-toplevel"], return_output=True, exit_on_failure=True)
assert root_dir
os.environ["LADYBIRD_SOURCE_DIR"] = root_dir
ladybird_source_dir = Path(root_dir)
return ladybird_source_dir
def build_main(build_dir: Path, target: Optional[str] = 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)
run_command(build_args, exit_on_failure=True)
def test_main(build_dir: Path, preset: str, pattern: Optional[str]):
test_args = [
"ctest",
"--preset",
preset,
"--output-on-failure",
"--test-dir",
str(build_dir),
]
if pattern:
test_args.extend(["-R", pattern])
run_command(test_args, exit_on_failure=True)
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)
run_command(run_args, exit_on_failure=True)
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)))
run_command(gdb_args, exit_on_failure=True)
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)
run_command(addr2line_args, exit_on_failure=True)
if __name__ == "__main__":
main()