From 3d0fdaacff72d3457f9c46346abc7ada011a7935 Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Tue, 27 May 2025 17:49:38 -0400 Subject: [PATCH] Meta: Migrate find_compiler.sh logic to a python script This will allow us to re-use this logic from within other python scripts. The find_compiler.sh script still exists, as it is used by some other bash scripts. The pick_host_compiler() function will now execute find_compiler.py and store its result in $CC and $CXX. Note that the python script supports Windows. --- Documentation/BuildInstructionsLadybird.md | 2 +- Meta/find_compiler.py | 193 +++++++++++++++++++++ Meta/find_compiler.sh | 80 ++------- Meta/utils.py | 41 +++++ 4 files changed, 247 insertions(+), 69 deletions(-) create mode 100755 Meta/find_compiler.py create mode 100644 Meta/utils.py diff --git a/Documentation/BuildInstructionsLadybird.md b/Documentation/BuildInstructionsLadybird.md index 0afdd45adfe..303103afc70 100644 --- a/Documentation/BuildInstructionsLadybird.md +++ b/Documentation/BuildInstructionsLadybird.md @@ -5,7 +5,7 @@ Qt6 development packages, nasm, additional build tools, and a C++23 capable compiler are required. We currently use gcc-14 and clang-20 in our CI pipeline. If these versions are not available on your system, see -[`Meta/find_compiler.sh`](../Meta/find_compiler.sh) for the minimum compatible version. +[`Meta/find_compiler.py`](../Meta/find_compiler.py) for the minimum compatible version. CMake 3.25 or newer must be available in $PATH. diff --git a/Meta/find_compiler.py b/Meta/find_compiler.py new file mode 100755 index 00000000000..73b5e61d42b --- /dev/null +++ b/Meta/find_compiler.py @@ -0,0 +1,193 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2025, Tim Flynn +# +# SPDX-License-Identifier: BSD-2-Clause + +import argparse +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.host_platform import HostSystem +from Meta.host_platform import Platform +from Meta.utils import run_command + + +CLANG_MINIMUM_VERSION = 17 +GCC_MINIMUM_VERSION = 13 +XCODE_MINIMUM_VERSION = ("14.3", 14030022) + +COMPILER_VERSION_REGEX = re.compile(r"(\d+)(\.\d+)*") + + +def major_compiler_version_if_supported(platform: Platform, compiler: str) -> Optional[int]: + if not shutil.which(compiler): + return None + + # On Windows, clang-cl is a driver that does not have the -dumpversion flag. We will use clang proper for this test. + if platform.host_system == HostSystem.Windows: + compiler = compiler.replace("clang-cl", "clang") + + version = run_command([compiler, "-dumpversion"], return_output=True) + if not version: + return None + + major_version = COMPILER_VERSION_REGEX.match(version) + if not major_version: + return None + + major_version = int(major_version.group(1)) + + version = run_command([compiler, "--version"], return_output=True) + if not version: + return None + + if platform.host_system == HostSystem.macOS and version.find("Apple clang") != -1: + apple_definitions = run_command([compiler, "-dM", "-E", "-"], input="", return_output=True) + if not apple_definitions: + return None + + apple_definitions = apple_definitions.split() + + try: + index = next(i for (i, v) in enumerate(apple_definitions) if "__apple_build_version__" in v) + apple_build_version = int(apple_definitions[index + 1]) + except (IndexError, StopIteration, ValueError): + return None + + if apple_build_version >= XCODE_MINIMUM_VERSION[1]: + # This inherently causes us to prefer Xcode clang over homebrew clang. + return apple_build_version + + elif version.find("clang") != -1: + if major_version >= CLANG_MINIMUM_VERSION: + return major_version + + else: + if major_version >= GCC_MINIMUM_VERSION: + return major_version + + return None + + +def find_newest_compiler(platform: Platform, compilers: list[str]) -> Optional[str]: + best_compiler = None + best_version = 0 + + for compiler in compilers: + major_version = major_compiler_version_if_supported(platform, compiler) + if not major_version: + continue + + if major_version > best_version: + best_version = major_version + best_compiler = compiler + + return best_compiler + + +def pick_host_compiler(platform: Platform, cc: str, cxx: str) -> tuple[str, str]: + if platform.host_system == HostSystem.Windows and ("clang-cl" not in cc or "clang-cl" not in cxx): + print( + f"clang-cl {CLANG_MINIMUM_VERSION} or higher is required on Windows", + file=sys.stderr, + ) + + sys.exit(1) + + # FIXME: Validate that the cc/cxx combination is compatible (e.g. don't allow CC=gcc and CXX=clang++) + if major_compiler_version_if_supported(platform, cc) and major_compiler_version_if_supported(platform, cxx): + return (cc, cxx) + + if platform.host_system == HostSystem.Windows: + clang_candidates = ["clang-cl"] + gcc_candidates = [] + else: + clang_candidates = [ + "clang", + "clang-17", + "clang-18", + "clang-19", + "clang-20", + ] + + gcc_candidates = [ + "gcc", + "gcc-13", + "gcc-14", + ] + + if platform.host_system == HostSystem.macOS: + clang_homebrew_path = Path("/opt/homebrew/opt/llvm/bin") + homebrew_path = Path("/opt/homebrew/bin") + + clang_candidates.extend([str(clang_homebrew_path.joinpath(c)) for c in clang_candidates]) + clang_candidates.extend([str(homebrew_path.joinpath(c)) for c in clang_candidates]) + + gcc_candidates.extend([str(homebrew_path.joinpath(c)) for c in gcc_candidates]) + elif platform.host_system == HostSystem.Linux: + local_path = Path("/usr/local/bin") + + clang_candidates.extend([str(local_path.joinpath(c)) for c in clang_candidates]) + gcc_candidates.extend([str(local_path.joinpath(c)) for c in gcc_candidates]) + + clang = find_newest_compiler(platform, clang_candidates) + if clang: + if platform.host_system == HostSystem.Windows: + return (clang, clang) + return clang, clang.replace("clang", "clang++") + + gcc = find_newest_compiler(platform, gcc_candidates) + if gcc: + return gcc, gcc.replace("gcc", "g++") + + if platform.host_system == HostSystem.macOS: + print( + f"Please ensure that Xcode {XCODE_MINIMUM_VERSION[0]}, Homebrew clang {CLANG_MINIMUM_VERSION}, or higher is installed", + file=sys.stderr, + ) + elif platform.host_system == HostSystem.Windows: + print( + f"Please ensure that clang-cl {CLANG_MINIMUM_VERSION} or higher is installed", + file=sys.stderr, + ) + else: + print( + f"Please ensure that clang {CLANG_MINIMUM_VERSION}, gcc {GCC_MINIMUM_VERSION}, or higher is installed", + file=sys.stderr, + ) + + sys.exit(1) + + +def default_host_compiler(platform: Platform) -> tuple[str, str]: + if platform.host_system == HostSystem.Windows: + return ("clang-cl", "clang-cl") + return ("cc", "c++") + + +def main(): + platform = Platform() + (default_cc, default_cxx) = default_host_compiler(platform) + + parser = argparse.ArgumentParser(description="Find valid compilers") + + parser.add_argument("--cc", required=False, default=default_cc) + parser.add_argument("--cxx", required=False, default=default_cxx) + + args = parser.parse_args() + + # The default action when this script is invoked is to provide the caller with content that may be evaluated by bash. + (cc, cxx) = pick_host_compiler(platform, args.cc, args.cxx) + print(f'export CC="{cc}"') + print(f'export CXX="{cxx}"') + + +if __name__ == "__main__": + main() diff --git a/Meta/find_compiler.sh b/Meta/find_compiler.sh index 320a0814248..258123a4c31 100644 --- a/Meta/find_compiler.sh +++ b/Meta/find_compiler.sh @@ -1,78 +1,22 @@ # shellcheck shell=bash -HOST_COMPILER="" - -is_supported_compiler() { - local COMPILER="$1" - if [ -z "$COMPILER" ]; then - return 1 - fi - - local VERSION="" - VERSION="$($COMPILER -dumpversion 2> /dev/null)" || return 1 - local MAJOR_VERSION="" - MAJOR_VERSION="${VERSION%%.*}" - if $COMPILER --version 2>&1 | grep "Apple clang" >/dev/null; then - # Apple Clang version check - BUILD_VERSION=$(echo | $COMPILER -dM -E - | grep __apple_build_version__ | cut -d ' ' -f3) - # Xcode 14.3, based on upstream LLVM 15 - [ "$BUILD_VERSION" -ge 14030022 ] && return 0 - elif $COMPILER --version 2>&1 | grep "clang" >/dev/null; then - # Clang version check - [ "$MAJOR_VERSION" -ge 17 ] && return 0 - else - # GCC version check - [ "$MAJOR_VERSION" -ge 13 ] && return 0 - fi - return 1 -} - -find_newest_compiler() { - local BEST_VERSION=0 - local BEST_CANDIDATE="" - for CANDIDATE in "$@"; do - if ! command -v "$CANDIDATE" >/dev/null 2>&1; then - continue - fi - if ! $CANDIDATE -dumpversion >/dev/null 2>&1; then - continue - fi - local VERSION="" - VERSION="$($CANDIDATE -dumpversion)" - local MAJOR_VERSION="${VERSION%%.*}" - if [ "$MAJOR_VERSION" -gt "$BEST_VERSION" ]; then - BEST_VERSION=$MAJOR_VERSION - BEST_CANDIDATE="$CANDIDATE" - fi - done - HOST_COMPILER=$BEST_CANDIDATE -} +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" pick_host_compiler() { - CC=${CC:-"cc"} - CXX=${CXX:-"c++"} + local output + local status - if is_supported_compiler "$CC" && is_supported_compiler "$CXX"; then - return + output=$("${DIR}/find_compiler.py") + status=$? + + if [[ ${status} -ne 0 ]] ; then + exit ${status} fi - find_newest_compiler clang clang-17 clang-18 clang-19 clang-20 /opt/homebrew/opt/llvm/bin/clang - if is_supported_compiler "$HOST_COMPILER"; then - export CC="${HOST_COMPILER}" - export CXX="${HOST_COMPILER/clang/clang++}" - return + if [[ "${output}" != *"CC="* || "${output}" != *"CXX="* ]] ; then + echo "Unexpected output from find_compiler.py" + exit 1 fi - find_newest_compiler egcc gcc gcc-13 gcc-14 /usr/local/bin/gcc-{13,14} /opt/homebrew/bin/gcc-{13,14} - if is_supported_compiler "$HOST_COMPILER"; then - export CC="${HOST_COMPILER}" - export CXX="${HOST_COMPILER/gcc/g++}" - return - fi - - if [ "$(uname -s)" = "Darwin" ]; then - die "Please make sure that Xcode 14.3, Homebrew Clang 17, or higher is installed." - else - die "Please make sure that GCC version 13, Clang version 17, or higher is installed." - fi + eval "${output}" } diff --git a/Meta/utils.py b/Meta/utils.py new file mode 100644 index 00000000000..4a01c2db7c3 --- /dev/null +++ b/Meta/utils.py @@ -0,0 +1,41 @@ +# Copyright (c) 2025, Tim Flynn +# +# SPDX-License-Identifier: BSD-2-Clause + +import signal +import subprocess +import sys + +from typing import Optional +from typing import Union + + +def run_command( + command: list[str], + input: Union[str, None] = None, + return_output: bool = False, + exit_on_failure: bool = False, +) -> Optional[str]: + stdin = subprocess.PIPE if type(input) is str else None + stdout = subprocess.PIPE if return_output else None + + try: + # FIXME: For Windows, set the working directory so DLLs are found. + with subprocess.Popen(command, stdin=stdin, stdout=stdout, text=True) as process: + (output, _) = process.communicate(input=input) + + if process.returncode != 0: + if exit_on_failure: + sys.exit(process.returncode) + return None + + except KeyboardInterrupt: + process.send_signal(signal.SIGINT) + process.wait() + + sys.exit(process.returncode) + + if return_output: + return output.strip() + + return None