# Copyright 2020 The Meson development team # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # http://www.apache.org/licenses/LICENSE-2.0 # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from pathlib import Path from .traceparser import CMakeTraceParser from ..envconfig import CMakeSkipCompilerTest from ..mesonlib import MachineChoice from ..compilers import VisualStudioLikeCompiler 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 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 = {} # type: 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 = { 'android': 'Android', 'linux': 'Linux', 'windows': 'Windows', 'freebsd': 'FreeBSD', 'darwin': 'Darwin', } # type: T.Dict[str, str] # 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 isinstance(compiler, VisualStudioLikeCompiler): 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)