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