1#!/usr/bin/env python3
2#
3# Copyright 2019 - The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9#     http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17"""It is an AIDEGen sub task: generate the CLion project file.
18
19    Usage example:
20    json_path = common_util.get_blueprint_json_path(
21        constant.BLUEPRINT_CC_JSONFILE_NAME)
22    json_dict = common_util.get_soong_build_json_dict(json_path)
23    if 'modules' not in json_dict:
24        return
25    mod_info = json_dict['modules'].get('libui', {})
26    if not mod_info:
27        return
28    CLionProjectFileGenerator(mod_info).generate_cmakelists_file()
29"""
30
31import logging
32import os
33
34from io import StringIO
35from io import TextIOWrapper
36
37from aidegen import constant
38from aidegen import templates
39from aidegen.lib import common_util
40from aidegen.lib import errors
41from aidegen.lib import native_module_info
42
43# Flags for writing to CMakeLists.txt section.
44_GLOBAL_COMMON_FLAGS = '\n# GLOBAL ALL FLAGS:\n'
45_LOCAL_COMMON_FLAGS = '\n# LOCAL ALL FLAGS:\n'
46_GLOBAL_CFLAGS = '\n# GLOBAL CFLAGS:\n'
47_LOCAL_CFLAGS = '\n# LOCAL CFLAGS:\n'
48_GLOBAL_C_ONLY_FLAGS = '\n# GLOBAL C ONLY FLAGS:\n'
49_LOCAL_C_ONLY_FLAGS = '\n# LOCAL C ONLY FLAGS:\n'
50_GLOBAL_CPP_FLAGS = '\n# GLOBAL CPP FLAGS:\n'
51_LOCAL_CPP_FLAGS = '\n# LOCAL CPP FLAGS:\n'
52_SYSTEM_INCLUDE_FLAGS = '\n# GLOBAL SYSTEM INCLUDE FLAGS:\n'
53
54# Keys for writing in module_bp_cc_deps.json
55_KEY_GLOBAL_COMMON_FLAGS = 'global_common_flags'
56_KEY_LOCAL_COMMON_FLAGS = 'local_common_flags'
57_KEY_GLOBAL_CFLAGS = 'global_c_flags'
58_KEY_LOCAL_CFLAGS = 'local_c_flags'
59_KEY_GLOBAL_C_ONLY_FLAGS = 'global_c_only_flags'
60_KEY_LOCAL_C_ONLY_FLAGS = 'local_c_only_flags'
61_KEY_GLOBAL_CPP_FLAGS = 'global_cpp_flags'
62_KEY_LOCAL_CPP_FLAGS = 'local_cpp_flags'
63_KEY_SYSTEM_INCLUDE_FLAGS = 'system_include_flags'
64
65# Dictionary maps keys to sections.
66_FLAGS_DICT = {
67    _KEY_GLOBAL_COMMON_FLAGS: _GLOBAL_COMMON_FLAGS,
68    _KEY_LOCAL_COMMON_FLAGS: _LOCAL_COMMON_FLAGS,
69    _KEY_GLOBAL_CFLAGS: _GLOBAL_CFLAGS,
70    _KEY_LOCAL_CFLAGS: _LOCAL_CFLAGS,
71    _KEY_GLOBAL_C_ONLY_FLAGS: _GLOBAL_C_ONLY_FLAGS,
72    _KEY_LOCAL_C_ONLY_FLAGS: _LOCAL_C_ONLY_FLAGS,
73    _KEY_GLOBAL_CPP_FLAGS: _GLOBAL_CPP_FLAGS,
74    _KEY_LOCAL_CPP_FLAGS: _LOCAL_CPP_FLAGS,
75    _KEY_SYSTEM_INCLUDE_FLAGS: _SYSTEM_INCLUDE_FLAGS
76}
77
78# Keys for parameter types.
79_KEY_FLAG = 'flag'
80_KEY_SYSTEM_ROOT = 'system_root'
81_KEY_RELATIVE = 'relative_file_path'
82
83# Constants for CMakeLists.txt.
84_MIN_VERSION_TOKEN = '@MINVERSION@'
85_PROJECT_NAME_TOKEN = '@PROJNAME@'
86_ANDOIR_ROOT_TOKEN = '@ANDROIDROOT@'
87_MINI_VERSION_SUPPORT = 'cmake_minimum_required(VERSION {})\n'
88_MINI_VERSION = '3.5'
89_KEY_CLANG = 'clang'
90_KEY_CPPLANG = 'clang++'
91_SET_C_COMPILER = 'set(CMAKE_C_COMPILER \"{}\")\n'
92_SET_CXX_COMPILER = 'set(CMAKE_CXX_COMPILER \"{}\")\n'
93_LIST_APPEND_HEADER = 'list(APPEND\n'
94_SOURCE_FILES_HEADER = 'SOURCE_FILES'
95_SOURCE_FILES_LINE = '     SOURCE_FILES\n'
96_END_WITH_ONE_BLANK_LINE = ')\n'
97_END_WITH_TWO_BLANK_LINES = ')\n\n'
98_SET_RELATIVE_PATH = 'set({} "{} {}={}")\n'
99_SET_ALL_FLAGS = 'set({} "{} {}")\n'
100_ANDROID_ROOT_SYMBOL = '${ANDROID_ROOT}'
101_SYSTEM = 'SYSTEM'
102_INCLUDE_DIR = 'include_directories({} \n'
103_SET_INCLUDE_FORMAT = '    "{}"\n'
104_CMAKE_C_FLAGS = 'CMAKE_C_FLAGS'
105_CMAKE_CXX_FLAGS = 'CMAKE_CXX_FLAGS'
106_USR = 'usr'
107_INCLUDE = 'include'
108_INCLUDE_SYSTEM = 'include_directories(SYSTEM "{}")\n'
109_GLOB_RECURSE_TMP_HEADERS = 'file (GLOB_RECURSE TMP_HEADERS\n'
110_ALL_HEADER_FILES = '    "{}/**/*.h"\n'
111_APPEND_SOURCE_FILES = "list (APPEND SOURCE_FILES ${TMP_HEADERS})\n\n"
112_ADD_EXECUTABLE_HEADER = '\nadd_executable({} {})'
113_PROJECT = 'project({})\n'
114_ADD_SUB = 'add_subdirectory({})\n'
115_DICT_EMPTY = 'mod_info is empty.'
116_DICT_NO_MOD_NAME_KEY = "mod_info does not contain 'module_name' key."
117_DICT_NO_PATH_KEY = "mod_info does not contain 'path' key."
118_MODULE_INFO_EMPTY = 'The module info dictionary is empty.'
119
120
121class CLionProjectFileGenerator:
122    """CLion project file generator.
123
124    Attributes:
125        mod_info: A dictionary of the target module's info.
126        mod_name: A string of module name.
127        mod_path: A string of module's path.
128        cc_dir: A string of generated CLion project file's directory.
129        cc_path: A string of generated CLion project file's path.
130    """
131
132    def __init__(self, mod_info):
133        """ProjectFileGenerator initialize.
134
135        Args:
136            mod_info: A dictionary of native module's info.
137        """
138        if not mod_info:
139            raise errors.ModuleInfoEmptyError(_MODULE_INFO_EMPTY)
140        self.mod_info = mod_info
141        self.mod_name = self._get_module_name()
142        self.mod_path = CLionProjectFileGenerator.get_module_path(mod_info)
143        self.cc_dir = CLionProjectFileGenerator.get_cmakelists_file_dir(
144            os.path.join(self.mod_path, self.mod_name))
145        if not os.path.exists(self.cc_dir):
146            os.makedirs(self.cc_dir)
147        self.cc_path = os.path.join(self.cc_dir,
148                                    constant.CLION_PROJECT_FILE_NAME)
149
150    def _get_module_name(self):
151        """Gets the value of the 'module_name' key if it exists.
152
153        Returns:
154            A string of the module's name.
155
156        Raises:
157            NoModuleNameDefinedInModuleInfoError if no 'module_name' key in
158            mod_info.
159        """
160        mod_name = self.mod_info.get(constant.KEY_MODULE_NAME, '')
161        if not mod_name:
162            raise errors.NoModuleNameDefinedInModuleInfoError(
163                _DICT_NO_MOD_NAME_KEY)
164        return mod_name
165
166    @staticmethod
167    def get_module_path(mod_info):
168        """Gets the first value of the 'path' key if it exists.
169
170        Args:
171            mod_info: A module's info dictionary.
172
173        Returns:
174            A string of the module's path.
175
176        Raises:
177            NoPathDefinedInModuleInfoError if no 'path' key in mod_info.
178        """
179        mod_paths = mod_info.get(constant.KEY_PATH, [])
180        if not mod_paths:
181            raise errors.NoPathDefinedInModuleInfoError(_DICT_NO_PATH_KEY)
182        return mod_paths[0]
183
184    @staticmethod
185    @common_util.check_args(cc_path=str)
186    def get_cmakelists_file_dir(cc_path):
187        """Gets module's CMakeLists.txt file path to be created.
188
189        Return a string of $OUT/development/ide/clion/${cc_path}.
190        For example, if module name is 'libui'. The return path string would be:
191            out/development/ide/clion/frameworks/native/libs/ui/libui
192
193        Args:
194            cc_path: A string of absolute path of module's Android.bp file.
195
196        Returns:
197            A string of absolute path of module's CMakeLists.txt file to be
198            created.
199        """
200        return os.path.join(common_util.get_android_root_dir(),
201                            common_util.get_android_out_dir(),
202                            constant.RELATIVE_NATIVE_PATH, cc_path)
203
204    def generate_cmakelists_file(self):
205        """Generates CLion project file from the target module's info."""
206        with open(self.cc_path, 'w') as hfile:
207            self._write_cmakelists_file(hfile)
208
209    @common_util.check_args(hfile=(TextIOWrapper, StringIO))
210    @common_util.io_error_handle
211    def _write_cmakelists_file(self, hfile):
212        """Writes CLion project file content with neccessary info.
213
214        Args:
215            hfile: A file handler instance.
216        """
217        self._write_header(hfile)
218        self._write_c_compiler_paths(hfile)
219        self._write_source_files(hfile)
220        self._write_cmakelists_flags(hfile)
221        self._write_tail(hfile)
222
223    @common_util.check_args(hfile=(TextIOWrapper, StringIO))
224    @common_util.io_error_handle
225    def _write_header(self, hfile):
226        """Writes CLion project file's header messages.
227
228        Args:
229            hfile: A file handler instance.
230        """
231        content = templates.CMAKELISTS_HEADER.replace(
232            _MIN_VERSION_TOKEN, _MINI_VERSION)
233        content = content.replace(_PROJECT_NAME_TOKEN, self.mod_name)
234        content = content.replace(
235            _ANDOIR_ROOT_TOKEN, common_util.get_android_root_dir())
236        hfile.write(content)
237
238    @common_util.check_args(hfile=(TextIOWrapper, StringIO))
239    @common_util.io_error_handle
240    def _write_c_compiler_paths(self, hfile):
241        """Writes CMake compiler paths for C and Cpp to CLion project file.
242
243        Args:
244            hfile: A file handler instance.
245        """
246        hfile.write(_SET_C_COMPILER.format(
247            native_module_info.NativeModuleInfo.c_lang_path))
248        hfile.write(_SET_CXX_COMPILER.format(
249            native_module_info.NativeModuleInfo.cpp_lang_path))
250
251    @common_util.check_args(hfile=(TextIOWrapper, StringIO))
252    @common_util.io_error_handle
253    def _write_source_files(self, hfile):
254        """Writes source files' paths to CLion project file.
255
256        Args:
257            hfile: A file handler instance.
258        """
259        if constant.KEY_SRCS not in self.mod_info:
260            logging.warning("No source files in %s's module info.",
261                            self.mod_name)
262            return
263        source_files = self.mod_info[constant.KEY_SRCS]
264        hfile.write(_LIST_APPEND_HEADER)
265        hfile.write(_SOURCE_FILES_LINE)
266        for src in source_files:
267            hfile.write(''.join([_build_cmake_path(src, '    '), '\n']))
268        hfile.write(_END_WITH_ONE_BLANK_LINE)
269
270    @common_util.check_args(hfile=(TextIOWrapper, StringIO))
271    @common_util.io_error_handle
272    def _write_cmakelists_flags(self, hfile):
273        """Writes all kinds of flags in CLion project file.
274
275        Args:
276            hfile: A file handler instance.
277        """
278        self._write_flags(hfile, _KEY_GLOBAL_COMMON_FLAGS, True, True)
279        self._write_flags(hfile, _KEY_LOCAL_COMMON_FLAGS, True, True)
280        self._write_flags(hfile, _KEY_GLOBAL_CFLAGS, True, True)
281        self._write_flags(hfile, _KEY_LOCAL_CFLAGS, True, True)
282        self._write_flags(hfile, _KEY_GLOBAL_C_ONLY_FLAGS, True, False)
283        self._write_flags(hfile, _KEY_LOCAL_C_ONLY_FLAGS, True, False)
284        self._write_flags(hfile, _KEY_GLOBAL_CPP_FLAGS, False, True)
285        self._write_flags(hfile, _KEY_LOCAL_CPP_FLAGS, False, True)
286        self._write_flags(hfile, _KEY_SYSTEM_INCLUDE_FLAGS, True, True)
287
288    @common_util.check_args(hfile=(TextIOWrapper, StringIO))
289    @common_util.io_error_handle
290    def _write_tail(self, hfile):
291        """Writes CLion project file content with necessary info.
292
293        Args:
294            hfile: A file handler instance.
295        """
296        hfile.write(
297            _ADD_EXECUTABLE_HEADER.format(
298                _cleanup_executable_name(self.mod_name),
299                _add_dollar_sign(_SOURCE_FILES_HEADER)))
300
301    @common_util.check_args(
302        hfile=(TextIOWrapper, StringIO), key=str, cflags=bool, cppflags=bool)
303    @common_util.io_error_handle
304    def _write_flags(self, hfile, key, cflags, cppflags):
305        """Writes CMake compiler paths of C, Cpp for different kinds of flags.
306
307        Args:
308            hfile: A file handler instance.
309            key: A string of flag type, e.g., 'global_common_flags' flag.
310            cflags: A boolean for setting 'CMAKE_C_FLAGS' flag.
311            cppflags: A boolean for setting 'CMAKE_CXX_FLAGS' flag.
312        """
313        if key not in _FLAGS_DICT:
314            return
315        hfile.write(_FLAGS_DICT[key])
316        params_dict = self._parse_compiler_parameters(key)
317        if params_dict:
318            _translate_to_cmake(hfile, params_dict, cflags, cppflags)
319
320    @common_util.check_args(flag=str)
321    def _parse_compiler_parameters(self, flag):
322        """Parses the specific flag data from a module_info dictionary.
323
324        Args:
325            flag: The string of key flag, e.g.: _KEY_GLOBAL_COMMON_FLAGS.
326
327        Returns:
328            A dictionary with compiled parameters.
329        """
330        params = self.mod_info.get(flag, {})
331        if not params:
332            return None
333        params_dict = {
334            constant.KEY_HEADER: [],
335            constant.KEY_SYSTEM: [],
336            _KEY_FLAG: [],
337            _KEY_SYSTEM_ROOT: '',
338            _KEY_RELATIVE: {}
339        }
340        for key, value in params.items():
341            params_dict[key] = value
342        return params_dict
343
344
345@common_util.check_args(rel_project_path=str, mod_names=list)
346@common_util.io_error_handle
347def generate_base_cmakelists_file(cc_module_info, rel_project_path, mod_names):
348    """Generates base CLion project file for multiple CLion projects.
349
350    We create a multiple native project file:
351    {android_root}/development/ide/clion/{rel_project_path}/CMakeLists.txt
352    and use this project file to generate a link:
353    {android_root}/out/development/ide/clion/{rel_project_path}/CMakeLists.txt
354
355    Args:
356        cc_module_info: An instance of native_module_info.NativeModuleInfo.
357        rel_project_path: A string of the base project relative path. For
358                          example: frameworks/native/libs/ui.
359        mod_names: A list of module names whose project were created under
360                   rel_project_path.
361
362    Returns:
363        A symbolic link CLion project file path.
364    """
365    root_dir = common_util.get_android_root_dir()
366    cc_dir = os.path.join(root_dir, constant.RELATIVE_NATIVE_PATH,
367                          rel_project_path)
368    cc_out_dir = os.path.join(root_dir, common_util.get_android_out_dir(),
369                              constant.RELATIVE_NATIVE_PATH, rel_project_path)
370    if not os.path.exists(cc_dir):
371        os.makedirs(cc_dir)
372    dst_path = os.path.join(cc_out_dir, constant.CLION_PROJECT_FILE_NAME)
373    if os.path.islink(dst_path):
374        os.unlink(dst_path)
375    src_path = os.path.join(cc_dir, constant.CLION_PROJECT_FILE_NAME)
376    if os.path.isfile(src_path):
377        os.remove(src_path)
378    with open(src_path, 'w') as hfile:
379        _write_base_cmakelists_file(hfile, cc_module_info, src_path, mod_names)
380    os.symlink(src_path, dst_path)
381    return dst_path
382
383
384@common_util.check_args(
385    hfile=(TextIOWrapper, StringIO), abs_project_path=str, mod_names=list)
386@common_util.io_error_handle
387def _write_base_cmakelists_file(hfile, cc_module_info, abs_project_path,
388                                mod_names):
389    """Writes base CLion project file content.
390
391    When we write module info into base CLion project file, first check if the
392    module's CMakeLists.txt exists. If file exists, write content,
393        add_subdirectory({'relative_module_path'})
394
395    Args:
396        hfile: A file handler instance.
397        cc_module_info: An instance of native_module_info.NativeModuleInfo.
398        abs_project_path: A string of the base project absolute path.
399                          For example,
400                              ${ANDROID_BUILD_TOP}/frameworks/native/libs/ui.
401        mod_names: A list of module names whose project were created under
402                   abs_project_path.
403    """
404    hfile.write(_MINI_VERSION_SUPPORT.format(_MINI_VERSION))
405    project_dir = os.path.dirname(abs_project_path)
406    hfile.write(_PROJECT.format(os.path.basename(project_dir)))
407    root_dir = common_util.get_android_root_dir()
408    for mod_name in mod_names:
409        mod_info = cc_module_info.get_module_info(mod_name)
410        mod_path = CLionProjectFileGenerator.get_module_path(mod_info)
411        file_dir = CLionProjectFileGenerator.get_cmakelists_file_dir(
412            os.path.join(mod_path, mod_name))
413        file_path = os.path.join(file_dir, constant.CLION_PROJECT_FILE_NAME)
414        if not os.path.isfile(file_path):
415            logging.warning("%s the project file %s doesn't exist.",
416                            common_util.COLORED_INFO('Warning:'), file_path)
417            continue
418        link_project_dir = os.path.join(root_dir,
419                                        common_util.get_android_out_dir(),
420                                        os.path.relpath(project_dir, root_dir))
421        rel_mod_path = os.path.relpath(file_dir, link_project_dir)
422        hfile.write(_ADD_SUB.format(rel_mod_path))
423
424
425@common_util.check_args(
426    hfile=(TextIOWrapper, StringIO), params_dict=dict, cflags=bool,
427    cppflags=bool)
428def _translate_to_cmake(hfile, params_dict, cflags, cppflags):
429    """Translates parameter dict's contents into CLion project file format.
430
431    Args:
432        hfile: A file handler instance.
433        params_dict: A dict contains data to be translated into CLion
434                     project file format.
435        cflags: A boolean is to set 'CMAKE_C_FLAGS' flag.
436        cppflags: A boolean is to set 'CMAKE_CXX_FLAGS' flag.
437    """
438    _write_all_include_directories(
439        hfile, params_dict[constant.KEY_SYSTEM], True)
440    _write_all_include_directories(
441        hfile, params_dict[constant.KEY_HEADER], False)
442
443    if cflags:
444        _write_all_relative_file_path_flags(hfile, params_dict[_KEY_RELATIVE],
445                                            _CMAKE_C_FLAGS)
446        _write_all_flags(hfile, params_dict[_KEY_FLAG], _CMAKE_C_FLAGS)
447
448    if cppflags:
449        _write_all_relative_file_path_flags(hfile, params_dict[_KEY_RELATIVE],
450                                            _CMAKE_CXX_FLAGS)
451        _write_all_flags(hfile, params_dict[_KEY_FLAG], _CMAKE_CXX_FLAGS)
452
453    if params_dict[_KEY_SYSTEM_ROOT]:
454        path = os.path.join(params_dict[_KEY_SYSTEM_ROOT], _USR, _INCLUDE)
455        hfile.write(_INCLUDE_SYSTEM.format(_build_cmake_path(path)))
456
457
458@common_util.check_args(hfile=(TextIOWrapper, StringIO), is_system=bool)
459@common_util.io_error_handle
460def _write_all_include_directories(hfile, includes, is_system):
461    """Writes all included directories' paths to the CLion project file.
462
463    Args:
464        hfile: A file handler instance.
465        includes: A list of included file paths.
466        is_system: A boolean of whether it's a system flag.
467    """
468    if not includes:
469        return
470    _write_all_includes(hfile, includes, is_system)
471    _write_all_headers(hfile, includes)
472
473
474@common_util.check_args(
475    hfile=(TextIOWrapper, StringIO), rel_paths_dict=dict, tag=str)
476@common_util.io_error_handle
477def _write_all_relative_file_path_flags(hfile, rel_paths_dict, tag):
478    """Writes all relative file path flags' parameters.
479
480    Args:
481        hfile: A file handler instance.
482        rel_paths_dict: A dict contains data of flag as a key and the relative
483                        path string as its value.
484        tag: A string of tag, such as 'CMAKE_C_FLAGS'.
485    """
486    for flag, path in rel_paths_dict.items():
487        hfile.write(
488            _SET_RELATIVE_PATH.format(tag, _add_dollar_sign(tag), flag,
489                                      _build_cmake_path(path)))
490
491
492@common_util.check_args(hfile=(TextIOWrapper, StringIO), flags=list, tag=str)
493@common_util.io_error_handle
494def _write_all_flags(hfile, flags, tag):
495    """Wrties all flags to the project file.
496
497    Args:
498        hfile: A file handler instance.
499        flags: A list of flag strings to be added.
500        tag: A string to be added a dollar sign.
501    """
502    for flag in flags:
503        hfile.write(_SET_ALL_FLAGS.format(tag, _add_dollar_sign(tag), flag))
504
505
506def _add_dollar_sign(tag):
507    """Adds dollar sign to a string, e.g.: 'ANDROID_ROOT' -> '${ANDROID_ROOT}'.
508
509    Args:
510        tag: A string to be added a dollar sign.
511
512    Returns:
513        A dollar sign added string.
514    """
515    return ''.join(['${', tag, '}'])
516
517
518def _build_cmake_path(path, tag=''):
519    """Joins tag, '${ANDROID_ROOT}' and path into a new string.
520
521    Args:
522        path: A string of a path.
523        tag: A string to be added in front of a dollar sign
524
525    Returns:
526        The composed string.
527    """
528    return ''.join([tag, _ANDROID_ROOT_SYMBOL, os.path.sep, path])
529
530
531@common_util.check_args(hfile=(TextIOWrapper, StringIO), is_system=bool)
532@common_util.io_error_handle
533def _write_all_includes(hfile, includes, is_system):
534    """Writes all included directories' paths to the CLion project file.
535
536    Args:
537        hfile: A file handler instance.
538        includes: A list of included file paths.
539        is_system: A boolean of whether it's a system flag.
540    """
541    if not includes:
542        return
543    system = ''
544    if is_system:
545        system = _SYSTEM
546    hfile.write(_INCLUDE_DIR.format(system))
547    for include in includes:
548        hfile.write(_SET_INCLUDE_FORMAT.format(_build_cmake_path(include)))
549    hfile.write(_END_WITH_TWO_BLANK_LINES)
550
551
552@common_util.check_args(hfile=(TextIOWrapper, StringIO))
553@common_util.io_error_handle
554def _write_all_headers(hfile, includes):
555    """Writes all header directories' paths to the CLion project file.
556
557    Args:
558        hfile: A file handler instance.
559        includes: A list of included file paths.
560    """
561    if not includes:
562        return
563    hfile.write(_GLOB_RECURSE_TMP_HEADERS)
564    for include in includes:
565        hfile.write(_ALL_HEADER_FILES.format(_build_cmake_path(include)))
566    hfile.write(_END_WITH_ONE_BLANK_LINE)
567    hfile.write(_APPEND_SOURCE_FILES)
568
569
570def _cleanup_executable_name(mod_name):
571    """Clean up an executable name to be suitable for CMake.
572
573    Replace the last '@' of a module name with '-' and make it become a suitable
574    executable name for CMake.
575
576    Args:
577        mod_name: A string of module name to be cleaned up.
578
579    Returns:
580        A string of the executable name.
581    """
582    return mod_name[::-1].replace('@', '-', 1)[::-1]
583