Files
ladybird/Meta/ladybird.py
Andrew Kaster 5c835157c6 Documentation+Meta: Remove references to nonexistent Toolchain directory
After b1d708dd16, the Toolchain directory
was removed from the repository. Some documentation and scripts still
referenced it, so this commit removes those references. The only
remaining references are in the gitignore file, to prevent bisections
from being polluted by the presence of a Toolchain directory in the
working copy.
2026-04-21 06:56:32 -04:00

482 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>
# Copyright (c) 2025, Nicolas Danelon <nicolasdanelon@gmail.com>
#
# SPDX-License-Identifier: BSD-2-Clause
import argparse
import os
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.build_vcpkg import build_vcpkg
from Meta.find_compiler import pick_host_compiler
from Meta.host_platform import GUIFramework
from Meta.host_platform import HostArchitecture
from Meta.host_platform import HostSystem
from Meta.host_platform import Platform
from Meta.utils import run_command
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", "Release"),
)
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)
compiler_parser.add_argument("--jobs", "-j", required=False)
compiler_parser.add_argument("--gui", required=False, choices=platform.valid_gui_frameworks())
target_parser = argparse.ArgumentParser(add_help=False)
target_parser.add_argument("target", nargs=argparse.OPTIONAL)
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", nargs=argparse.OPTIONAL, help="Limits the tests that are run 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, compiler_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"
)
debug_parser.add_argument(
"args", nargs=argparse.REMAINDER, help="Additional arguments passed through to the build system"
)
profile_parser = subparsers.add_parser(
"profile",
help="Launches the application under callgrind for performance profiling",
parents=[preset_parser, compiler_parser, target_parser],
)
profile_parser.add_argument(
"args", nargs=argparse.REMAINDER, help="Additional arguments passed through to the application"
)
install_parser = subparsers.add_parser(
"install", help="Installs the target binary", parents=[preset_parser, compiler_parser, target_parser]
)
install_parser.add_argument(
"args", nargs=argparse.REMAINDER, help="Additional arguments passed through to the build system"
)
subparsers.add_parser("vcpkg", help="Ensure that dependencies are available", parents=[preset_parser])
subparsers.add_parser("clean", help="Cleans the build environment", parents=[preset_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 "target" in args:
if platform.host_system != HostSystem.Windows and args.target == "ladybird":
args.target = "Ladybird"
if not args.target and args.command not in ("build", "rebuild"):
args.target = "ladybird" if platform.host_system == HostSystem.Windows else "Ladybird"
if args.command == "build":
build_dir = configure_main(platform, args.preset, args.cc, args.cxx, args.gui)
build_main(build_dir, args.jobs, args.target, args.args)
elif args.command == "test":
build_dir = configure_main(platform, args.preset, args.cc, args.cxx, args.gui)
build_main(build_dir, args.jobs)
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, args.gui)
build_main(build_dir, args.jobs, 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, args.gui)
build_main(build_dir, args.jobs, args.target, args.args)
debug_main(platform.host_system, build_dir, args.target, args.debugger, args.cmd)
elif args.command == "profile":
build_dir = configure_main(platform, args.preset, args.cc, args.cxx, args.gui)
build_main(build_dir, args.jobs, args.target)
profile_main(platform.host_system, build_dir, args.target, args.args)
elif args.command == "install":
build_dir = configure_main(platform, args.preset, args.cc, args.cxx, args.gui)
build_main(build_dir, args.jobs, args.target, args.args)
build_main(build_dir, args.jobs, "install", args.args)
elif args.command == "vcpkg":
configure_build_env(platform, args.preset)
build_vcpkg()
elif args.command == "clean":
clean_main(platform, args.preset)
elif args.command == "rebuild":
clean_main(platform, args.preset)
build_dir = configure_main(platform, args.preset, args.cc, args.cxx, args.gui)
build_main(build_dir, args.jobs, args.target, args.args)
elif args.command == "addr2line":
build_dir = configure_main(platform, args.preset, args.cc, args.cxx, args.gui)
build_main(build_dir, args.jobs, args.target)
addr2line_main(build_dir, args.target, args.program, args.addresses)
def configure_main(platform: Platform, preset: str, cc: str, cxx: str, gui: Optional[GUIFramework]) -> Path:
ladybird_source_dir, build_preset_dir = configure_build_env(platform, preset)
build_vcpkg()
if build_preset_dir.joinpath("build.ninja").exists() or build_preset_dir.joinpath("ladybird.sln").exists():
if not gui or gui == gui_for_build_dir(build_preset_dir):
return build_preset_dir
if not gui:
gui = platform.default_gui_framework()
validate_cmake_version()
(cc, cxx) = pick_host_compiler(platform, cc, cxx)
config_args = [
"cmake",
"--preset",
preset,
"-S",
ladybird_source_dir,
"-B",
build_preset_dir,
f"-DCMAKE_C_COMPILER={cc}",
f"-DCMAKE_CXX_COMPILER={cxx}",
f"-DLADYBIRD_GUI_FRAMEWORK={gui}",
]
if platform.host_system == HostSystem.Linux and platform.host_architecture == HostArchitecture.AArch64:
config_args.extend(configure_skia_jemalloc())
# 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 gui_for_build_dir(build_preset_dir: Path) -> Optional[GUIFramework]:
cmake_cachefile = build_preset_dir.joinpath("CMakeCache.txt")
if not cmake_cachefile.exists():
return None
with cmake_cachefile.open("r") as f:
for line in f:
if line.startswith("LADYBIRD_GUI_FRAMEWORK:STRING="):
try:
return GUIFramework(line.strip().split("=", 1)[1])
except ValueError:
return None
return None
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})\n",
f"set(GN {gn})\n",
"\n",
]
)
return cmake_args
def configure_build_env(platform: Platform, preset: str) -> tuple[Path, Path]:
ladybird_source_dir = ensure_ladybird_source_dir()
build_root_dir = ladybird_source_dir / "Build"
known_presets = {
"Debug": build_root_dir / "debug",
"All_Debug": build_root_dir / "alldebug",
"Distribution": build_root_dir / "distribution",
"Release": build_root_dir / "release",
"Sanitizer": build_root_dir / "sanitizers",
"Host_Tools": build_root_dir / "host-tools-build",
}
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["PATH"] += os.pathsep + vcpkg_root
os.environ["VCPKG_ROOT"] = vcpkg_root
if platform.host_architecture == HostArchitecture.riscv64:
# vcpkg refuses to build ports on riscv64 and other less common architectures without this flag.
# With it set vcpkg will use the system provided CMake and Ninja binaries but will still download,
# build and use its own pinned versions of gn, meson and pkg-config.
os.environ["VCPKG_FORCE_SYSTEM_BINARIES"] = "1"
if "XDG_CACHE_HOME" not in os.environ:
# vcpkg requires this variable to set and in some cases like the Android build environment it might
# not be present, so we need to ensure that it is set
os.environ["XDG_CACHE_HOME"] = str(ladybird_source_dir / "Build" / "caches")
return ladybird_source_dir, build_preset_dir
def validate_cmake_version():
cmake_install_message = "Please install CMake version 3.30 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 < 30):
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 is_running_under_coding_agent() -> bool:
return "CLAUDECODE" in os.environ or "CODEX_SANDBOX" in os.environ
def build_main(build_dir: Path, jobs: Optional[str], target: Optional[str] = None, args: Optional[list[str]] = None):
build_args = ["ninja", "-C", str(build_dir)]
if is_running_under_coding_agent():
build_args.append("--quiet")
if not jobs:
jobs = os.environ.get("MAKEJOBS", None)
if jobs:
build_args.extend(["-j", jobs])
if target:
build_args.append(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 (
"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")))
elif host_system == HostSystem.Windows:
gdb_args.append(str(build_dir.joinpath("bin", target + ".exe")))
else:
gdb_args.append(str(build_dir.joinpath("bin", target)))
run_command(gdb_args, exit_on_failure=True)
def profile_main(host_system: HostSystem, build_dir: Path, target: str, args: list[str]):
if not shutil.which("valgrind"):
print("Please install valgrind", file=sys.stderr)
sys.exit(1)
valgrind_args = ["valgrind", "--tool=callgrind", "--instr-atstart=yes"]
if target == "Ladybird" and host_system == HostSystem.macOS:
valgrind_args.append(str(build_dir.joinpath("bin", "Ladybird.app")))
else:
valgrind_args.append(str(build_dir.joinpath("bin", target)))
valgrind_args.extend(args)
run_command(valgrind_args, exit_on_failure=True)
def clean_main(platform: Platform, preset: str):
ladybird_source_dir, build_preset_dir = configure_build_env(platform, preset)
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()