| # SPDX-License-Identifier: Apache-2.0 |
| # Copyright 2020 The Meson development team |
| |
| from __future__ import annotations |
| |
| from pathlib import Path |
| from .traceparser import CMakeTraceParser |
| from ..envconfig import CMakeSkipCompilerTest |
| from .common import language_map, cmake_get_generator_args |
| from .. import mlog |
| |
| import shutil |
| import typing as T |
| from enum import Enum |
| from textwrap import dedent |
| |
| if T.TYPE_CHECKING: |
| from .executor import CMakeExecutor |
| from ..environment import Environment |
| from ..compilers import Compiler |
| from ..mesonlib import MachineChoice |
| |
| class CMakeExecScope(Enum): |
| SUBPROJECT = 'subproject' |
| DEPENDENCY = 'dependency' |
| |
| class CMakeToolchain: |
| def __init__(self, cmakebin: 'CMakeExecutor', env: 'Environment', for_machine: MachineChoice, exec_scope: CMakeExecScope, build_dir: Path, preload_file: T.Optional[Path] = None) -> None: |
| self.env = env |
| self.cmakebin = cmakebin |
| self.for_machine = for_machine |
| self.exec_scope = exec_scope |
| self.preload_file = preload_file |
| self.build_dir = build_dir |
| self.build_dir = self.build_dir.resolve() |
| self.toolchain_file = build_dir / 'CMakeMesonToolchainFile.cmake' |
| self.cmcache_file = build_dir / 'CMakeCache.txt' |
| self.minfo = self.env.machines[self.for_machine] |
| self.properties = self.env.properties[self.for_machine] |
| self.compilers = self.env.coredata.compilers[self.for_machine] |
| self.cmakevars = self.env.cmakevars[self.for_machine] |
| self.cmakestate = self.env.coredata.cmake_cache[self.for_machine] |
| |
| self.variables = self.get_defaults() |
| self.variables.update(self.cmakevars.get_variables()) |
| |
| # Determine whether CMake the compiler test should be skipped |
| skip_status = self.properties.get_cmake_skip_compiler_test() |
| self.skip_check = skip_status == CMakeSkipCompilerTest.ALWAYS |
| if skip_status == CMakeSkipCompilerTest.DEP_ONLY and self.exec_scope == CMakeExecScope.DEPENDENCY: |
| self.skip_check = True |
| if not self.properties.get_cmake_defaults(): |
| self.skip_check = False |
| |
| assert self.toolchain_file.is_absolute() |
| |
| def write(self) -> Path: |
| if not self.toolchain_file.parent.exists(): |
| self.toolchain_file.parent.mkdir(parents=True) |
| self.toolchain_file.write_text(self.generate(), encoding='utf-8') |
| self.cmcache_file.write_text(self.generate_cache(), encoding='utf-8') |
| mlog.cmd_ci_include(self.toolchain_file.as_posix()) |
| return self.toolchain_file |
| |
| def get_cmake_args(self) -> T.List[str]: |
| args = ['-DCMAKE_TOOLCHAIN_FILE=' + self.toolchain_file.as_posix()] |
| if self.preload_file is not None: |
| args += ['-DMESON_PRELOAD_FILE=' + self.preload_file.as_posix()] |
| return args |
| |
| @staticmethod |
| def _print_vars(vars: T.Dict[str, T.List[str]]) -> str: |
| res = '' |
| for key, value in vars.items(): |
| res += 'set(' + key |
| for i in value: |
| res += f' "{i}"' |
| res += ')\n' |
| return res |
| |
| def generate(self) -> str: |
| res = dedent('''\ |
| ###################################### |
| ### AUTOMATICALLY GENERATED FILE ### |
| ###################################### |
| |
| # This file was generated from the configuration in the |
| # relevant meson machine file. See the meson documentation |
| # https://mesonbuild.com/Machine-files.html for more information |
| |
| if(DEFINED MESON_PRELOAD_FILE) |
| include("${MESON_PRELOAD_FILE}") |
| endif() |
| |
| ''') |
| |
| # Escape all \ in the values |
| for key, value in self.variables.items(): |
| self.variables[key] = [x.replace('\\', '/') for x in value] |
| |
| # Set compiler |
| if self.skip_check: |
| self.update_cmake_compiler_state() |
| res += '# CMake compiler state variables\n' |
| for lang, vars in self.cmakestate: |
| res += f'# -- Variables for language {lang}\n' |
| res += self._print_vars(vars) |
| res += '\n' |
| res += '\n' |
| |
| # Set variables from the current machine config |
| res += '# Variables from meson\n' |
| res += self._print_vars(self.variables) |
| res += '\n' |
| |
| # Add the user provided toolchain file |
| user_file = self.properties.get_cmake_toolchain_file() |
| if user_file is not None: |
| res += dedent(''' |
| # Load the CMake toolchain file specified by the user |
| include("{}") |
| |
| '''.format(user_file.as_posix())) |
| |
| return res |
| |
| def generate_cache(self) -> str: |
| if not self.skip_check: |
| return '' |
| |
| res = '' |
| for name, v in self.cmakestate.cmake_cache.items(): |
| res += f'{name}:{v.type}={";".join(v.value)}\n' |
| return res |
| |
| def get_defaults(self) -> T.Dict[str, T.List[str]]: |
| defaults: T.Dict[str, T.List[str]] = {} |
| |
| # Do nothing if the user does not want automatic defaults |
| if not self.properties.get_cmake_defaults(): |
| return defaults |
| |
| # Best effort to map the meson system name to CMAKE_SYSTEM_NAME, which |
| # is not trivial since CMake lacks a list of all supported |
| # CMAKE_SYSTEM_NAME values. |
| SYSTEM_MAP: T.Dict[str, str] = { |
| 'android': 'Android', |
| 'linux': 'Linux', |
| 'windows': 'Windows', |
| 'freebsd': 'FreeBSD', |
| 'darwin': 'Darwin', |
| } |
| |
| # Only set these in a cross build. Otherwise CMake will trip up in native |
| # builds and thing they are cross (which causes TRY_RUN() to break) |
| if self.env.is_cross_build(when_building_for=self.for_machine): |
| defaults['CMAKE_SYSTEM_NAME'] = [SYSTEM_MAP.get(self.minfo.system, self.minfo.system)] |
| defaults['CMAKE_SYSTEM_PROCESSOR'] = [self.minfo.cpu_family] |
| |
| defaults['CMAKE_SIZEOF_VOID_P'] = ['8' if self.minfo.is_64_bit else '4'] |
| |
| sys_root = self.properties.get_sys_root() |
| if sys_root: |
| defaults['CMAKE_SYSROOT'] = [sys_root] |
| |
| def make_abs(exe: str) -> str: |
| if Path(exe).is_absolute(): |
| return exe |
| |
| p = shutil.which(exe) |
| if p is None: |
| return exe |
| return p |
| |
| # Set the compiler variables |
| for lang, comp_obj in self.compilers.items(): |
| prefix = 'CMAKE_{}_'.format(language_map.get(lang, lang.upper())) |
| |
| exe_list = comp_obj.get_exelist() |
| if not exe_list: |
| continue |
| |
| if len(exe_list) >= 2 and not self.is_cmdline_option(comp_obj, exe_list[1]): |
| defaults[prefix + 'COMPILER_LAUNCHER'] = [make_abs(exe_list[0])] |
| exe_list = exe_list[1:] |
| |
| exe_list[0] = make_abs(exe_list[0]) |
| defaults[prefix + 'COMPILER'] = exe_list |
| if comp_obj.get_id() == 'clang-cl': |
| defaults['CMAKE_LINKER'] = comp_obj.get_linker_exelist() |
| |
| return defaults |
| |
| @staticmethod |
| def is_cmdline_option(compiler: 'Compiler', arg: str) -> bool: |
| if compiler.get_argument_syntax() == 'msvc': |
| return arg.startswith('/') |
| else: |
| return arg.startswith('-') |
| |
| def update_cmake_compiler_state(self) -> None: |
| # Check if all variables are already cached |
| if self.cmakestate.languages.issuperset(self.compilers.keys()): |
| return |
| |
| # Generate the CMakeLists.txt |
| mlog.debug('CMake Toolchain: Calling CMake once to generate the compiler state') |
| languages = list(self.compilers.keys()) |
| lang_ids = [language_map.get(x, x.upper()) for x in languages] |
| cmake_content = dedent(f''' |
| cmake_minimum_required(VERSION 3.7) |
| project(CompInfo {' '.join(lang_ids)}) |
| ''') |
| |
| build_dir = Path(self.env.scratch_dir) / '__CMake_compiler_info__' |
| build_dir.mkdir(parents=True, exist_ok=True) |
| cmake_file = build_dir / 'CMakeLists.txt' |
| cmake_file.write_text(cmake_content, encoding='utf-8') |
| |
| # Generate the temporary toolchain file |
| temp_toolchain_file = build_dir / 'CMakeMesonTempToolchainFile.cmake' |
| temp_toolchain_file.write_text(CMakeToolchain._print_vars(self.variables), encoding='utf-8') |
| |
| # Configure |
| trace = CMakeTraceParser(self.cmakebin.version(), build_dir, self.env) |
| self.cmakebin.set_exec_mode(print_cmout=False, always_capture_stderr=trace.requires_stderr()) |
| cmake_args = [] |
| cmake_args += trace.trace_args() |
| cmake_args += cmake_get_generator_args(self.env) |
| cmake_args += [f'-DCMAKE_TOOLCHAIN_FILE={temp_toolchain_file.as_posix()}', '.'] |
| rc, _, raw_trace = self.cmakebin.call(cmake_args, build_dir=build_dir, disable_cache=True) |
| |
| if rc != 0: |
| mlog.warning('CMake Toolchain: Failed to determine CMake compilers state') |
| return |
| |
| # Parse output |
| trace.parse(raw_trace) |
| self.cmakestate.cmake_cache = {**trace.cache} |
| |
| vars_by_file = {k.name: v for (k, v) in trace.vars_by_file.items()} |
| |
| for lang in languages: |
| lang_cmake = language_map.get(lang, lang.upper()) |
| file_name = f'CMake{lang_cmake}Compiler.cmake' |
| vars = vars_by_file.setdefault(file_name, {}) |
| vars[f'CMAKE_{lang_cmake}_COMPILER_FORCED'] = ['1'] |
| self.cmakestate.update(lang, vars) |