aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/project/cmake/toolchain.py
blob: 7966192702fdc2a3540be6fcdc8d652891ab4ba3 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
# Copyright (c) 2020 Egor Tensin <Egor.Tensin@gmail.com>
# This file is part of the "cmake-common" project.
# For details, see https://github.com/egor-tensin/cmake-common.
# Distributed under the MIT License.

# Default generator
# -----------------
#
# As of CMake 3.18, the default generator (unless set explicitly) is:
#   * the newest Visual Studio or "NMake Makefiles" on Windows,
#   * "Unix Makefiles" otherwise.
# This is regardless of whether any executables like gcc, cl or make are
# available [1].
#
# Makefile generators
# -------------------
#
# CMake has a number of "... Makefiles" generators.  "Unix Makefiles" uses
# gmake/make/smake, whichever is found first, and cc/c++ for compiler
# detection [2].  "MinGW Makefiles" looks for mingw32-make.exe in a number of
# well-known locations, uses gcc/g++ directly, and is aware of windres [3].  In
# addition, "Unix Makefiles" uses /bin/sh as the SHELL value in the Makefile,
# while the MinGW version uses cmd.exe.  I don't think it matters on Windows
# though, since the non-existent /bin/sh is ignored anyway [4].  "NMake
# Makefiles" is similar, except it defaults to using cl [5].
#
# It's important to _not_ use the -A parameter with any of the Makefile
# generators - it's an error.  This goes for "NMake Makefiles" also.  "NMake
# Makefiles" doesn't attempt to search for installed Visual Studio compilers,
# you need to use it from one of the Visual Studio-provided shells.
#
# Visual Studio generators
# ------------------------
#
# These are special.  They ignore the CMAKE_<LANG>_COMPILER parameters and use
# cl by default [9].  They support specifying the toolset to use via the -T
# parameter (the "Platform Toolset" value in the project's properties) since
# 3.18 [10].  The toolset list varies between Visual Studio versions, and I'm
# too lazy to learn exactly which version supports which toolsets.
#
# cmake --build uses msbuild with Visual Studio generators.  You can pass the
# path to a different cl.exe by doing something like
#
#     msbuild ... /p:CLToolExe=another-cl.exe /p:CLToolPath=C:\parent\dir
#
# It's important that the generators for Visual Studio 2017 or older use Win32
# Win32 as the default platform [12].  Because of that, we need to pass the -A
# parameter.
#
# mingw32-make vs make
# --------------------
#
# No idea what the actual differences are.  The explanation in the FAQ [6]
# about how GNU make "is lacking in some functionality and has modified
# functionality due to the lack of POSIX on Win32" isn't terribly helpful.
#
# It's important that you can install either on Windows (`choco install make`
# for GNU make and `choco install mingw` to install a MinGW-w64 distribution
# with mingw32-make.exe included).  Personally, I don't see any difference
# between using either make.exe or mingw32-make.exe w/ CMake on Windows.  But,
# since MinGW-w64 distributions do include mingw32-make.exe and not make.exe,
# we'll try to detect that.
#
# Cross-compilation
# -----------------
#
# If you want to e.g. build x86 binary on x64 and vice versa, the easiest way
# seems to be to make a CMake "toolchain file", which initializes the proper
# compiler flags (like -m64/-m32, etc.).  Such file could look like this:
#
#     set(CMAKE_C_COMPILER   gcc)
#     set(CMAKE_C_FLAGS      -m32)
#     set(CMAKE_CXX_COMPILER g++)
#     set(CMAKE_CXX_FLAGS    -m32)
#
# You can then pass the path to it using the CMAKE_TOOLCHAIN_FILE parameter.
#
# If you use the Visual Studio generators, just use the -A parameter, like "-A
# Win32".
#
# As a side note, if you want to cross-compile between x86 and x64 using GCC on
# Ubuntu, you need to install the gcc-multilib package.
#
# Windows & Clang
# ---------------
#
# Using Clang on Windows is no easy task, of course.  Prior to 3.15, there was
# no support for building things using the clang++.exe executable, only
# clang-cl.exe was supported [7].  If you specified -DCMAKE_CXX_COMPILER=clang++,
# CMake would stil pass MSVC-style command line options to the compiler (like
# /MD, /nologo, etc.), which clang++ doesn't like [8].
#
# So, in summary, you can only use clang++ since 3.15.  clang-cl doesn't work
# with Visual Studio generators unless you specify the proper toolset using the
# -T parameter.  You can set the ClToolExe property using msbuild, but while
# that might work in practice, clang-cl.exe needs to map some unsupported
# options for everything to work properly.  For an example of how this is done,
# see the LLVM.Cpp.Common.* files at [11].
#
# I recommend using Clang (either clang-cl or clang++ since 3.15) using the
# "NMake Makefiles" generator.
#
# References
# ----------
#
# [1]: cmake::EvaluateDefaultGlobalGenerator
#      https://github.com/Kitware/CMake/blob/v3.18.4/Source/cmake.cxx
# [2]: https://github.com/Kitware/CMake/blob/v3.18.4/Source/cmGlobalUnixMakefileGenerator3.cxx
# [3]: https://github.com/Kitware/CMake/blob/v3.18.4/Source/cmGlobalMinGWMakefileGenerator.cxx
# [4]: https://www.gnu.org/software/make/manual/html_node/Choosing-the-Shell.html
# [5]: https://github.com/Kitware/CMake/blob/v3.18.4/Source/cmGlobalNMakeMakefileGenerator.cxx
# [6]: http://mingw.org/wiki/FAQ
# [7]: https://cmake.org/cmake/help/v3.15/release/3.15.html#compilers
# [8]: https://github.com/Kitware/CMake/blob/v3.14.7/Modules/Platform/Windows-Clang.cmake
# [9]: https://gitlab.kitware.com/cmake/cmake/-/issues/19174
# [10]: https://cmake.org/cmake/help/v3.8/release/3.8.html
# [11]: https://github.com/llvm/llvm-project/tree/e408935bb5339e20035d84307c666fbdd15e99e0/llvm/tools/msbuild
# [12]: https://cmake.org/cmake/help/v3.18/generator/Visual%20Studio%2015%202017.html

import abc
import os.path
import shutil

import project.mingw
from project.os import on_windows
from project.toolchain import ToolchainType


class Toolchain(abc.ABC):
    @abc.abstractmethod
    def get_cmake_args(self):
        pass

    @abc.abstractmethod
    def get_build_args(self):
        pass

    @staticmethod
    def detect(hint, platform, build_dir):
        if hint is ToolchainType.AUTO:
            if on_windows():
                # We need to specify the -A parameter.  This might break if
                # none of the Visual Studio generators are available, but the
                # NMake one is, although I don't know how this can be possible
                # normally.
                hint = ToolchainType.MSVC
            else:
                # Same thing for the -m32/-m64 flags.
                hint = ToolchainType.GCC
        if hint is ToolchainType.MSVC:
            return MSVC(platform)
        if hint is ToolchainType.GCC:
            return GCC.setup(platform, build_dir)
        if hint is ToolchainType.MINGW:
            return MinGW.setup(platform, build_dir)
        if hint is ToolchainType.CLANG:
            return Clang.setup(platform, build_dir)
        if hint is ToolchainType.CLANG_CL:
            return ClangCL.setup(platform, build_dir)
        raise NotImplementedError(f'unrecognized toolset: {hint}')


class Auto(Toolchain):
    def get_cmake_args(self):
        return []

    def get_build_args(self):
        return []


class MSVC(Auto):
    def __init__(self, platform):
        self.platform = platform

    def get_cmake_args(self):
        # This doesn't actually specify the generator of course, but I don't
        # want to implement VS detection logic.
        return ['-A', self.platform.get_cmake_arch()]

    def get_build_args(self):
        return ['/m']


class Makefile(Toolchain):
    def __init__(self, path):
        self.path = path

    @staticmethod
    def _get_config_path(build_dir):
        return os.path.join(build_dir, 'custom_toolchain.cmake')

    @staticmethod
    def _get_makefile_generator():
        if on_windows():
            if shutil.which('mingw32-make'):
                return 'MinGW Makefiles'
            return 'Unix Makefiles'
        # On Linux/Cygwin, make all the way:
        return 'Unix Makefiles'

    @classmethod
    def write_config(cls, build_dir, contents):
        path = Makefile._get_config_path(build_dir)
        with open(path, mode='w') as file:
            file.write(contents)
        return cls(path)

    def get_cmake_args(self):
        return [
            '-D', f'CMAKE_TOOLCHAIN_FILE={self.path}',
            # The Visual Studio generator is the default on Windows, override
            # it:
            '-G', self._get_makefile_generator(),
        ]

    def get_build_args(self):
        return []


class GCC(Makefile):
    @staticmethod
    def _format(platform):
        return f'''
set(CMAKE_C_COMPILER   gcc)
set(CMAKE_C_FLAGS      -m{platform.get_address_model()})
set(CMAKE_CXX_COMPILER g++)
set(CMAKE_CXX_FLAGS    -m{platform.get_address_model()})
'''

    @staticmethod
    def setup(platform, build_dir):
        return GCC.write_config(build_dir, GCC._format(platform))


class MinGW(Makefile):
    @staticmethod
    def _format(platform):
        return f'''
set(CMAKE_C_COMPILER   {project.mingw.get_gcc(platform)})
set(CMAKE_CXX_COMPILER {project.mingw.get_gxx(platform)})
set(CMAKE_AR           {project.mingw.get_ar(platform)})
set(CMAKE_RANLIB       {project.mingw.get_ranlib(platform)})
set(CMAKE_RC_COMPILER  {project.mingw.get_windres(platform)})
set(CMAKE_SYSTEM_NAME  Windows)
'''

    @staticmethod
    def setup(platform, build_dir):
        return MinGW.write_config(build_dir, MinGW._format(platform))


class Clang(Makefile):
    @staticmethod
    def _format(platform):
        return f'''
if(CMAKE_VERSION VERSION_LESS "3.15" AND WIN32)
    set(CMAKE_C_COMPILER   clang-cl)
    set(CMAKE_CXX_COMPILER clang-cl)
    set(CMAKE_C_FLAGS      -m{platform.get_address_model()})
    set(CMAKE_CXX_FLAGS    -m{platform.get_address_model()})
else()
    set(CMAKE_C_COMPILER   clang)
    set(CMAKE_CXX_COMPILER clang++)
    set(CMAKE_C_FLAGS      -m{platform.get_address_model()})
    set(CMAKE_CXX_FLAGS    -m{platform.get_address_model()})
endif()
'''

    def _get_makefile_generator(self):
        if on_windows():
            # MinGW utilities like make might be unavailable, but NMake can
            # very much be there.
            if shutil.which('nmake'):
                return 'NMake Makefiles'
        return super()._get_makefile_generator()

    @staticmethod
    def setup(platform, build_dir):
        return Clang.write_config(build_dir, Clang._format(platform))


class ClangCL(Clang):
    @staticmethod
    def _format(platform):
        return f'''
set(CMAKE_C_COMPILER   clang-cl)
set(CMAKE_CXX_COMPILER clang-cl)
set(CMAKE_C_FLAGS      -m{platform.get_address_model()})
set(CMAKE_CXX_FLAGS    -m{platform.get_address_model()})
set(CMAKE_SYSTEM_NAME  Windows)
'''

    @staticmethod
    def setup(platform, build_dir):
        return ClangCL.write_config(build_dir, ClangCL._format(platform))