1"""
2 * Copyright (c) 2006-2025 RT-Thread Development Team
3 *
4 * SPDX-License-Identifier: Apache-2.0
5 *
6 * Change Logs:
7 * Date           Author       Notes
8 * 2019-05-24     klivelinux   first version
9 * 2021-04-19     liukangcc    add c++ support and libpath
10 * 2021-06-25     Guozhanxin   fix path issue
11 * 2021-06-30     Guozhanxin   add scons --target=cmake-armclang
12 * 2022-03-16     liukangcc    通过 SCons生成 CMakefile.txt 使用相对路径
13 * 2022-04-12     mysterywolf  rtconfig.CROSS_TOOL->rtconfig.PLATFORM
14 * 2022-04-29     SunJun8      默认开启生成编译数据库
15 * 2024-03-18     wirano       fix the issue of the missing link flags added in Sconscript
16 * 2024-07-04     kaidegit     Let cmake generator get more param from `rtconfig.py`
17 * 2024-08-07     imi415       Updated CMake generator handles private macros, using OBJECT and INTERFACE libraries.
18 * 2024-11-18     kaidegit     fix processing groups with similar name
19 * 2025-02-22     kaidegit     fix missing some flags added in Sconscript
20 * 2025-02-24     kaidegit     remove some code that is unnecessary but takes time, get them from env
21"""
22
23import os
24import sys
25import re
26import utils
27import rtconfig
28from utils import _make_path_relative
29from collections import defaultdict, Counter
30
31
32def GenerateCFiles(env, project, project_name):
33    """
34    Generate CMakeLists.txt files
35    """
36
37    PROJECT_NAME = project_name if project_name != "project" else "rtthread"
38
39    tool_path_conv = defaultdict(lambda : {"name":"", "path": ""})
40    tool_path_conv_helper = lambda tool: {"name": tool, "path": os.path.join(rtconfig.EXEC_PATH, tool).replace('\\', "/")}
41
42    tool_path_conv["CMAKE_C_COMPILER"] = tool_path_conv_helper(rtconfig.CC)
43    if 'CXX' in dir(rtconfig):
44        tool_path_conv["CMAKE_CXX_COMPILER"] = tool_path_conv_helper(rtconfig.CXX)
45    tool_path_conv["CMAKE_ASM_COMPILER"] = tool_path_conv_helper(rtconfig.AS)
46    tool_path_conv["CMAKE_AR"] = tool_path_conv_helper(rtconfig.AR)
47    tool_path_conv["CMAKE_LINKER"] = tool_path_conv_helper(rtconfig.LINK)
48    if rtconfig.PLATFORM in ['gcc']:
49        tool_path_conv["CMAKE_SIZE"] = tool_path_conv_helper(rtconfig.SIZE)
50        tool_path_conv["CMAKE_OBJDUMP"] = tool_path_conv_helper(rtconfig.OBJDUMP)
51        tool_path_conv["CMAKE_OBJCOPY"] = tool_path_conv_helper(rtconfig.OBJCPY)
52    elif rtconfig.PLATFORM in ['armcc', 'armclang']:
53        tool_path_conv["CMAKE_FROMELF"] = tool_path_conv_helper(rtconfig.FROMELF)
54
55    CC = tool_path_conv["CMAKE_C_COMPILER"]["path"]
56    CXX = tool_path_conv["CMAKE_CXX_COMPILER"]["path"]
57    AS = tool_path_conv["CMAKE_ASM_COMPILER"]["path"]
58    AR = tool_path_conv["CMAKE_AR"]["path"]
59    LINK = tool_path_conv["CMAKE_LINKER"]["path"]
60    SIZE = tool_path_conv["CMAKE_SIZE"]["path"]
61    OBJDUMP = tool_path_conv["CMAKE_OBJDUMP"]["path"]
62    OBJCOPY = tool_path_conv["CMAKE_OBJCOPY"]["path"]
63    FROMELF = tool_path_conv["CMAKE_FROMELF"]["path"]
64
65    CFLAGS = env['CFLAGS'].replace('\\', "/").replace('\"', "\\\"")
66    if 'CXXFLAGS' in dir(rtconfig):
67        cflag_str=''.join(env['CXXFLAGS'])
68        CXXFLAGS = cflag_str.replace('\\', "/").replace('\"', "\\\"")
69    else:
70        CXXFLAGS = CFLAGS
71    AFLAGS = env['ASFLAGS'].replace('\\', "/").replace('\"', "\\\"")
72    LFLAGS = env['LINKFLAGS'].replace('\\', "/").replace('\"', "\\\"")
73
74    POST_ACTION = rtconfig.POST_ACTION
75    # replace the tool name with the cmake variable
76    for cmake_var, each_tool in tool_path_conv.items():
77        tool_name = each_tool['name']
78        if tool_name == "": continue
79        if "win32" in sys.platform:
80            while f"{tool_name}.exe" in POST_ACTION:    # find the tool with `.exe` suffix first
81                POST_ACTION = POST_ACTION.replace(tool_name, "string_to_replace")
82        while tool_name in POST_ACTION:
83            POST_ACTION = POST_ACTION.replace(tool_name, "string_to_replace")
84        while "string_to_replace" in POST_ACTION:
85            POST_ACTION = POST_ACTION.replace("string_to_replace", f"${{{cmake_var}}}")
86    # replace the `$TARGET` with `${CMAKE_PROJECT_NAME}.elf`
87    while "$TARGET" in POST_ACTION:
88        POST_ACTION = POST_ACTION.replace("$TARGET", "${CMAKE_PROJECT_NAME}.elf")
89    # add COMMAAND before each command
90    POST_ACTION = POST_ACTION.split('\n')
91    POST_ACTION = [each_line.strip() for each_line in POST_ACTION]
92    POST_ACTION = [f"\tCOMMAND {each_line}" for each_line in POST_ACTION if each_line != '']
93    POST_ACTION = "\n".join(POST_ACTION)
94
95    if "win32" in sys.platform:
96        CC += ".exe"
97        if CXX != '':
98            CXX += ".exe"
99        AS += ".exe"
100        AR += ".exe"
101        LINK += ".exe"
102        if rtconfig.PLATFORM in ['gcc']:
103            SIZE += ".exe"
104            OBJDUMP += ".exe"
105            OBJCOPY += ".exe"
106        elif rtconfig.PLATFORM in ['armcc', 'armclang']:
107            FROMELF += ".exe"
108
109    if not os.path.exists(CC) or not os.path.exists(AS) or not os.path.exists(AR) or not os.path.exists(LINK):
110        print("'Cannot found toolchain directory, please check RTT_CC and RTT_EXEC_PATH'")
111        sys.exit(-1)
112
113    with open("CMakeLists.txt", "w") as cm_file:
114        cm_file.write("CMAKE_MINIMUM_REQUIRED(VERSION 3.10)\n\n")
115
116        cm_file.write("SET(CMAKE_SYSTEM_NAME Generic)\n")
117        cm_file.write("SET(CMAKE_SYSTEM_PROCESSOR " + rtconfig.CPU +")\n")
118        cm_file.write("#SET(CMAKE_VERBOSE_MAKEFILE ON)\n\n")
119        cm_file.write("SET(CMAKE_EXPORT_COMPILE_COMMANDS ON)\n\n")
120
121        cm_file.write("SET(CMAKE_C_COMPILER \""+ CC + "\")\n")
122        cm_file.write("SET(CMAKE_ASM_COMPILER \""+ AS + "\")\n")
123        cm_file.write("SET(CMAKE_C_FLAGS \""+ CFLAGS + "\")\n")
124        cm_file.write("SET(CMAKE_ASM_FLAGS \""+ AFLAGS + "\")\n")
125        cm_file.write("SET(CMAKE_C_COMPILER_WORKS TRUE)\n\n")
126
127        if CXX != '':
128            cm_file.write("SET(CMAKE_CXX_COMPILER \""+ CXX + "\")\n")
129            cm_file.write("SET(CMAKE_CXX_FLAGS \""+ CXXFLAGS + "\")\n")
130            cm_file.write("SET(CMAKE_CXX_COMPILER_WORKS TRUE)\n\n")
131
132        if rtconfig.PLATFORM in ['gcc']:
133            cm_file.write("SET(CMAKE_OBJCOPY \""+ OBJCOPY + "\")\n")
134            cm_file.write("SET(CMAKE_SIZE \""+ SIZE + "\")\n\n")
135        elif rtconfig.PLATFORM in ['armcc', 'armclang']:
136            cm_file.write("SET(CMAKE_FROMELF \""+ FROMELF + "\")\n\n")
137
138        LINKER_FLAGS = ''
139        LINKER_LIBS = ''
140        if rtconfig.PLATFORM in ['gcc']:
141            LINKER_FLAGS += '-T'
142        elif rtconfig.PLATFORM in ['armcc', 'armclang']:
143            LINKER_FLAGS += '--scatter'
144            for group in project:
145                if 'LIBPATH' in group.keys():
146                    for f in group['LIBPATH']:
147                        LINKER_LIBS += ' --userlibpath ' + f.replace("\\", "/")
148            for group in project:
149                if 'LIBS' in group.keys():
150                    for f in group['LIBS']:
151                        LINKER_LIBS += ' ' + f.replace("\\", "/") + '.lib'
152        cm_file.write("SET(CMAKE_EXE_LINKER_FLAGS \""+ re.sub(LINKER_FLAGS + r'(\s*)', LINKER_FLAGS + r' ${CMAKE_SOURCE_DIR}/', LFLAGS) + LINKER_LIBS + "\")\n\n")
153
154        # get the c/cpp standard version from compilation flags
155        # not support the version with alphabet in `-std` param yet
156        pattern = re.compile(r'-std=[\w+]+')
157        c_standard = 11
158        if '-std=' in CFLAGS:
159            c_standard = re.search(pattern, CFLAGS).group(0)
160            c_standard = "".join([each for each in c_standard if each.isdigit()])
161        else:
162            print(f"Cannot find the param of the c standard in build flag, set to default {c_standard}")
163        cm_file.write(f"SET(CMAKE_C_STANDARD {c_standard})\n")
164
165        if CXX != '':
166            cpp_standard = 17
167            if '-std=' in CXXFLAGS:
168                cpp_standard = re.search(pattern, CXXFLAGS).group(0)
169                cpp_standard = "".join([each for each in cpp_standard if each.isdigit()])
170            else:
171                print(f"Cannot find the param of the cpp standard in build flag, set to default {cpp_standard}")
172            cm_file.write(f"SET(CMAKE_CXX_STANDARD {cpp_standard})\n")
173
174        cm_file.write('\n')
175
176        cm_file.write(f"PROJECT({PROJECT_NAME} C {'CXX' if CXX != '' else ''} ASM)\n")
177
178        cm_file.write('\n')
179
180        cm_file.write("INCLUDE_DIRECTORIES(\n")
181        for i in env['CPPPATH']:
182            # use relative path
183            path = _make_path_relative(os.getcwd(), i)
184            cm_file.write( "\t" + path.replace("\\", "/") + "\n")
185        cm_file.write(")\n\n")
186
187        cm_file.write("ADD_DEFINITIONS(\n")
188        for i in env['CPPDEFINES']:
189            cm_file.write("\t-D" + i + "\n")
190        cm_file.write(")\n\n")
191
192        libgroups = []
193        interfacelibgroups = []
194        for group in project:
195            if group['name'] == 'Applications':
196                continue
197
198            # When a group is provided without sources, add it to the <INTERFACE> library list
199            if len(group['src']) == 0:
200                interfacelibgroups.append(group)
201            else:
202                libgroups.append(group)
203
204        # Process groups whose names differ only in capitalization.
205        # (Groups have same name should be merged into one before)
206        for group in libgroups:
207            group['alias'] = group['name'].lower()
208        names = [group['alias'] for group in libgroups]
209        counter = Counter(names)
210        names = [name for name in names if counter[name] > 1]
211        for group in libgroups:
212            if group['alias'] in names:
213                counter[group['alias']] -= 1
214                group['alias'] = f"{group['name']}_{counter[group['alias']]}"
215                print(f"Renamed {group['name']} to {group['alias']}")
216                group['name'] = group['alias']
217
218        cm_file.write("# Library source files\n")
219        for group in project:
220            cm_file.write("SET(RT_{:s}_SOURCES\n".format(group['name'].upper()))
221            for f in group['src']:
222                # use relative path
223                path = _make_path_relative(os.getcwd(), os.path.normpath(f.rfile().abspath))
224                cm_file.write( "\t" + path.replace("\\", "/") + "\n" )
225            cm_file.write(")\n\n")
226
227        cm_file.write("# Library search paths\n")
228        for group in libgroups + interfacelibgroups:
229            if not 'LIBPATH' in group.keys():
230                continue
231
232            if len(group['LIBPATH']) == 0:
233                continue
234
235            cm_file.write("SET(RT_{:s}_LINK_DIRS\n".format(group['name'].upper()))
236            for f in group['LIBPATH']:
237                cm_file.write("\t"+ f.replace("\\", "/") + "\n" )
238            cm_file.write(")\n\n")
239
240        cm_file.write("# Library local macro definitions\n")
241        for group in libgroups:
242            if not 'LOCAL_CPPDEFINES' in group.keys():
243                continue
244
245            if len(group['LOCAL_CPPDEFINES']) == 0:
246                continue
247
248            cm_file.write("SET(RT_{:s}_DEFINES\n".format(group['name'].upper()))
249            for f in group['LOCAL_CPPDEFINES']:
250                cm_file.write("\t"+ f.replace("\\", "/") + "\n" )
251            cm_file.write(")\n\n")
252
253        cm_file.write("# Library dependencies\n")
254        for group in libgroups + interfacelibgroups:
255            if not 'LIBS' in group.keys():
256                continue
257
258            if len(group['LIBS']) == 0:
259                continue
260
261            cm_file.write("SET(RT_{:s}_LIBS\n".format(group['name'].upper()))
262            for f in group['LIBS']:
263                cm_file.write("\t"+ "{}\n".format(f.replace("\\", "/")))
264            cm_file.write(")\n\n")
265
266        cm_file.write("# Libraries\n")
267        for group in libgroups:
268            cm_file.write("ADD_LIBRARY(rtt_{:s} OBJECT ${{RT_{:s}_SOURCES}})\n"
269                          .format(group['name'], group['name'].upper()))
270
271        cm_file.write("\n")
272
273        cm_file.write("# Interface libraries\n")
274        for group in interfacelibgroups:
275            cm_file.write("ADD_LIBRARY(rtt_{:s} INTERFACE)\n".format(group['name']))
276
277        cm_file.write("\n")
278
279        cm_file.write("# Private macros\n")
280        for group in libgroups:
281            if not 'LOCAL_CPPDEFINES' in group.keys():
282                continue
283
284            if len(group['LOCAL_CPPDEFINES']) == 0:
285                continue
286
287            cm_file.write("TARGET_COMPILE_DEFINITIONS(rtt_{:s} PRIVATE ${{RT_{:s}_DEFINES}})\n"
288              .format(group['name'], group['name'].upper()))
289
290        cm_file.write("\n")
291
292        cm_file.write("# Interface library search paths\n")
293        if rtconfig.PLATFORM in ['gcc']:
294            for group in libgroups:
295                if not 'LIBPATH' in group.keys():
296                    continue
297
298                if len(group['LIBPATH']) == 0:
299                    continue
300
301                cm_file.write("TARGET_LINK_DIRECTORIES(rtt_{:s} INTERFACE ${{RT_{:s}_LINK_DIRS}})\n"
302                              .format(group['name'], group['name'].upper()))
303
304            for group in libgroups:
305                if not 'LIBS' in group.keys():
306                    continue
307
308                if len(group['LIBS']) == 0:
309                    continue
310
311                cm_file.write("TARGET_LINK_LIBRARIES(rtt_{:s} INTERFACE ${{RT_{:s}_LIBS}})\n"
312                              .format(group['name'], group['name'].upper()))
313
314        cm_file.write("\n")
315
316        cm_file.write("ADD_EXECUTABLE(${CMAKE_PROJECT_NAME}.elf ${RT_APPLICATIONS_SOURCES})\n")
317
318        cm_file.write("TARGET_LINK_LIBRARIES(${CMAKE_PROJECT_NAME}.elf\n")
319        for group in libgroups + interfacelibgroups:
320            cm_file.write("\trtt_{:s}\n".format(group['name']))
321        cm_file.write(")\n\n")
322
323        cm_file.write("ADD_CUSTOM_COMMAND(TARGET ${CMAKE_PROJECT_NAME}.elf POST_BUILD \n" + POST_ACTION + '\n)\n')
324
325        # auto inclue `custom.cmake` for user custom settings
326        custom_cmake = \
327            '''
328            # if custom.cmake is exist, add it
329            if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/custom.cmake)
330                include(${CMAKE_CURRENT_SOURCE_DIR}/custom.cmake)
331            endif()
332            '''
333        custom_cmake = custom_cmake.split('\n')
334        custom_cmake = [each.strip() for each in custom_cmake]
335        custom_cmake = "\n".join(custom_cmake)
336        cm_file.write(custom_cmake)
337
338    return
339
340def CMakeProject(env, project, project_name):
341    print('Update setting files for CMakeLists.txt...')
342    GenerateCFiles(env, project, project_name)
343    print('Done!')
344
345    return
346