| 
									
										
										
										
											2025-05-27 17:49:38 -04:00
										 |  |  | #!/usr/bin/env python3 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | # Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org> | 
					
						
							|  |  |  | # | 
					
						
							|  |  |  | # 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", | 
					
						
							|  |  |  |         ] | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-05-29 13:12:59 -04:00
										 |  |  |         if platform.host_system == HostSystem.BSD: | 
					
						
							|  |  |  |             gcc_candidates.append("egcc") | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-05-27 17:49:38 -04:00
										 |  |  |     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]) | 
					
						
							| 
									
										
										
										
											2025-05-29 13:12:59 -04:00
										 |  |  |     elif platform.host_system in (HostSystem.Linux, HostSystem.BSD): | 
					
						
							| 
									
										
										
										
											2025-05-27 17:49:38 -04:00
										 |  |  |         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) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-09 18:32:00 -06:00
										 |  |  | def pick_swift_compilers(platform: Platform, project_root: Path) -> tuple[Path, Path, Path]: | 
					
						
							|  |  |  |     if platform.host_system == HostSystem.Windows: | 
					
						
							|  |  |  |         print("Swift builds are not supported on Windows", file=sys.stderr) | 
					
						
							|  |  |  |         sys.exit(1) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if not shutil.which("swiftly"): | 
					
						
							|  |  |  |         print("swiftly is required to manage Swift toolchains", file=sys.stderr) | 
					
						
							|  |  |  |         sys.exit(1) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     swiftly_toolchain_path = run_command(["swiftly", "use", "--print-location"], return_output=True, cwd=project_root) | 
					
						
							|  |  |  |     if not swiftly_toolchain_path: | 
					
						
							|  |  |  |         run_command(["swiftly", "install"], exit_on_failure=True, cwd=project_root) | 
					
						
							|  |  |  |         swiftly_toolchain_path = run_command( | 
					
						
							|  |  |  |             ["swiftly", "use", "--print-location"], return_output=True, exit_on_failure=True, cwd=project_root | 
					
						
							|  |  |  |         ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     swiftly_toolchain_path = Path(swiftly_toolchain_path.strip()) | 
					
						
							|  |  |  |     swiftly_bin_dir = swiftly_toolchain_path.joinpath("usr", "bin") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if not swiftly_toolchain_path.exists() or not swiftly_bin_dir.exists(): | 
					
						
							|  |  |  |         print(f"swiftly toolchain path {swiftly_toolchain_path} does not exist", file=sys.stderr) | 
					
						
							|  |  |  |         sys.exit(1) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return swiftly_bin_dir / "clang", swiftly_bin_dir / "clang++", swiftly_bin_dir / "swiftc" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-05-27 17:49:38 -04:00
										 |  |  | def main(): | 
					
						
							|  |  |  |     platform = Platform() | 
					
						
							| 
									
										
										
										
											2025-05-29 13:12:59 -04:00
										 |  |  |     (default_cc, default_cxx) = platform.default_compiler() | 
					
						
							| 
									
										
										
										
											2025-05-27 17:49:38 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  |     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() |