1# Copyright 2020 The Meson development team 2 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6 7# http://www.apache.org/licenses/LICENSE-2.0 8 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15from pathlib import Path 16from .traceparser import CMakeTraceParser 17from ..envconfig import CMakeSkipCompilerTest 18from ..mesonlib import MachineChoice 19from ..compilers import VisualStudioLikeCompiler 20from .common import language_map, cmake_get_generator_args 21from .. import mlog 22 23import shutil 24import typing as T 25from enum import Enum 26from textwrap import dedent 27 28if T.TYPE_CHECKING: 29 from .executor import CMakeExecutor 30 from ..environment import Environment 31 from ..compilers import Compiler 32 33class CMakeExecScope(Enum): 34 SUBPROJECT = 'subproject' 35 DEPENDENCY = 'dependency' 36 37class CMakeToolchain: 38 def __init__(self, cmakebin: 'CMakeExecutor', env: 'Environment', for_machine: MachineChoice, exec_scope: CMakeExecScope, build_dir: Path, preload_file: T.Optional[Path] = None) -> None: 39 self.env = env 40 self.cmakebin = cmakebin 41 self.for_machine = for_machine 42 self.exec_scope = exec_scope 43 self.preload_file = preload_file 44 self.build_dir = build_dir 45 self.build_dir = self.build_dir.resolve() 46 self.toolchain_file = build_dir / 'CMakeMesonToolchainFile.cmake' 47 self.cmcache_file = build_dir / 'CMakeCache.txt' 48 self.minfo = self.env.machines[self.for_machine] 49 self.properties = self.env.properties[self.for_machine] 50 self.compilers = self.env.coredata.compilers[self.for_machine] 51 self.cmakevars = self.env.cmakevars[self.for_machine] 52 self.cmakestate = self.env.coredata.cmake_cache[self.for_machine] 53 54 self.variables = self.get_defaults() 55 self.variables.update(self.cmakevars.get_variables()) 56 57 # Determine whether CMake the compiler test should be skipped 58 skip_status = self.properties.get_cmake_skip_compiler_test() 59 self.skip_check = skip_status == CMakeSkipCompilerTest.ALWAYS 60 if skip_status == CMakeSkipCompilerTest.DEP_ONLY and self.exec_scope == CMakeExecScope.DEPENDENCY: 61 self.skip_check = True 62 if not self.properties.get_cmake_defaults(): 63 self.skip_check = False 64 65 assert self.toolchain_file.is_absolute() 66 67 def write(self) -> Path: 68 if not self.toolchain_file.parent.exists(): 69 self.toolchain_file.parent.mkdir(parents=True) 70 self.toolchain_file.write_text(self.generate(), encoding='utf-8') 71 self.cmcache_file.write_text(self.generate_cache(), encoding='utf-8') 72 mlog.cmd_ci_include(self.toolchain_file.as_posix()) 73 return self.toolchain_file 74 75 def get_cmake_args(self) -> T.List[str]: 76 args = ['-DCMAKE_TOOLCHAIN_FILE=' + self.toolchain_file.as_posix()] 77 if self.preload_file is not None: 78 args += ['-DMESON_PRELOAD_FILE=' + self.preload_file.as_posix()] 79 return args 80 81 @staticmethod 82 def _print_vars(vars: T.Dict[str, T.List[str]]) -> str: 83 res = '' 84 for key, value in vars.items(): 85 res += 'set(' + key 86 for i in value: 87 res += f' "{i}"' 88 res += ')\n' 89 return res 90 91 def generate(self) -> str: 92 res = dedent('''\ 93 ###################################### 94 ### AUTOMATICALLY GENERATED FILE ### 95 ###################################### 96 97 # This file was generated from the configuration in the 98 # relevant meson machine file. See the meson documentation 99 # https://mesonbuild.com/Machine-files.html for more information 100 101 if(DEFINED MESON_PRELOAD_FILE) 102 include("${MESON_PRELOAD_FILE}") 103 endif() 104 105 ''') 106 107 # Escape all \ in the values 108 for key, value in self.variables.items(): 109 self.variables[key] = [x.replace('\\', '/') for x in value] 110 111 # Set compiler 112 if self.skip_check: 113 self.update_cmake_compiler_state() 114 res += '# CMake compiler state variables\n' 115 for lang, vars in self.cmakestate: 116 res += f'# -- Variables for language {lang}\n' 117 res += self._print_vars(vars) 118 res += '\n' 119 res += '\n' 120 121 # Set variables from the current machine config 122 res += '# Variables from meson\n' 123 res += self._print_vars(self.variables) 124 res += '\n' 125 126 # Add the user provided toolchain file 127 user_file = self.properties.get_cmake_toolchain_file() 128 if user_file is not None: 129 res += dedent(''' 130 # Load the CMake toolchain file specified by the user 131 include("{}") 132 133 '''.format(user_file.as_posix())) 134 135 return res 136 137 def generate_cache(self) -> str: 138 if not self.skip_check: 139 return '' 140 141 res = '' 142 for name, v in self.cmakestate.cmake_cache.items(): 143 res += f'{name}:{v.type}={";".join(v.value)}\n' 144 return res 145 146 def get_defaults(self) -> T.Dict[str, T.List[str]]: 147 defaults = {} # type: T.Dict[str, T.List[str]] 148 149 # Do nothing if the user does not want automatic defaults 150 if not self.properties.get_cmake_defaults(): 151 return defaults 152 153 # Best effort to map the meson system name to CMAKE_SYSTEM_NAME, which 154 # is not trivial since CMake lacks a list of all supported 155 # CMAKE_SYSTEM_NAME values. 156 SYSTEM_MAP = { 157 'android': 'Android', 158 'linux': 'Linux', 159 'windows': 'Windows', 160 'freebsd': 'FreeBSD', 161 'darwin': 'Darwin', 162 } # type: T.Dict[str, str] 163 164 # Only set these in a cross build. Otherwise CMake will trip up in native 165 # builds and thing they are cross (which causes TRY_RUN() to break) 166 if self.env.is_cross_build(when_building_for=self.for_machine): 167 defaults['CMAKE_SYSTEM_NAME'] = [SYSTEM_MAP.get(self.minfo.system, self.minfo.system)] 168 defaults['CMAKE_SYSTEM_PROCESSOR'] = [self.minfo.cpu_family] 169 170 defaults['CMAKE_SIZEOF_VOID_P'] = ['8' if self.minfo.is_64_bit else '4'] 171 172 sys_root = self.properties.get_sys_root() 173 if sys_root: 174 defaults['CMAKE_SYSROOT'] = [sys_root] 175 176 def make_abs(exe: str) -> str: 177 if Path(exe).is_absolute(): 178 return exe 179 180 p = shutil.which(exe) 181 if p is None: 182 return exe 183 return p 184 185 # Set the compiler variables 186 for lang, comp_obj in self.compilers.items(): 187 prefix = 'CMAKE_{}_'.format(language_map.get(lang, lang.upper())) 188 189 exe_list = comp_obj.get_exelist() 190 if not exe_list: 191 continue 192 193 if len(exe_list) >= 2 and not self.is_cmdline_option(comp_obj, exe_list[1]): 194 defaults[prefix + 'COMPILER_LAUNCHER'] = [make_abs(exe_list[0])] 195 exe_list = exe_list[1:] 196 197 exe_list[0] = make_abs(exe_list[0]) 198 defaults[prefix + 'COMPILER'] = exe_list 199 if comp_obj.get_id() == 'clang-cl': 200 defaults['CMAKE_LINKER'] = comp_obj.get_linker_exelist() 201 202 return defaults 203 204 @staticmethod 205 def is_cmdline_option(compiler: 'Compiler', arg: str) -> bool: 206 if isinstance(compiler, VisualStudioLikeCompiler): 207 return arg.startswith('/') 208 else: 209 return arg.startswith('-') 210 211 def update_cmake_compiler_state(self) -> None: 212 # Check if all variables are already cached 213 if self.cmakestate.languages.issuperset(self.compilers.keys()): 214 return 215 216 # Generate the CMakeLists.txt 217 mlog.debug('CMake Toolchain: Calling CMake once to generate the compiler state') 218 languages = list(self.compilers.keys()) 219 lang_ids = [language_map.get(x, x.upper()) for x in languages] 220 cmake_content = dedent(f''' 221 cmake_minimum_required(VERSION 3.7) 222 project(CompInfo {' '.join(lang_ids)}) 223 ''') 224 225 build_dir = Path(self.env.scratch_dir) / '__CMake_compiler_info__' 226 build_dir.mkdir(parents=True, exist_ok=True) 227 cmake_file = build_dir / 'CMakeLists.txt' 228 cmake_file.write_text(cmake_content, encoding='utf-8') 229 230 # Generate the temporary toolchain file 231 temp_toolchain_file = build_dir / 'CMakeMesonTempToolchainFile.cmake' 232 temp_toolchain_file.write_text(CMakeToolchain._print_vars(self.variables), encoding='utf-8') 233 234 # Configure 235 trace = CMakeTraceParser(self.cmakebin.version(), build_dir) 236 self.cmakebin.set_exec_mode(print_cmout=False, always_capture_stderr=trace.requires_stderr()) 237 cmake_args = [] 238 cmake_args += trace.trace_args() 239 cmake_args += cmake_get_generator_args(self.env) 240 cmake_args += [f'-DCMAKE_TOOLCHAIN_FILE={temp_toolchain_file.as_posix()}', '.'] 241 rc, _, raw_trace = self.cmakebin.call(cmake_args, build_dir=build_dir, disable_cache=True) 242 243 if rc != 0: 244 mlog.warning('CMake Toolchain: Failed to determine CMake compilers state') 245 return 246 247 # Parse output 248 trace.parse(raw_trace) 249 self.cmakestate.cmake_cache = {**trace.cache} 250 251 vars_by_file = {k.name: v for (k, v) in trace.vars_by_file.items()} 252 253 for lang in languages: 254 lang_cmake = language_map.get(lang, lang.upper()) 255 file_name = f'CMake{lang_cmake}Compiler.cmake' 256 vars = vars_by_file.setdefault(file_name, {}) 257 vars[f'CMAKE_{lang_cmake}_COMPILER_FORCED'] = ['1'] 258 self.cmakestate.update(lang, vars) 259