1#!/usr/bin/env python
2
3# SPDX-License-Identifier: Unlicense
4#
5# Based on the template file provided by the 'YCM-Generator' project authored by
6# Reuben D'Netto.
7# Jiahui Xie has re-reformatted and expanded the original script in accordance
8# to the requirements of the PEP 8 style guide and 'systemd' project,
9# respectively.
10#
11# The original license is preserved as it is.
12#
13#
14# This is free and unencumbered software released into the public domain.
15#
16# Anyone is free to copy, modify, publish, use, compile, sell, or
17# distribute this software, either in source code form or as a compiled
18# binary, for any purpose, commercial or non-commercial, and by any
19# means.
20#
21# In jurisdictions that recognize copyright laws, the author or authors
22# of this software dedicate any and all copyright interest in the
23# software to the public domain. We make this dedication for the benefit
24# of the public at large and to the detriment of our heirs and
25# successors. We intend this dedication to be an overt act of
26# relinquishment in perpetuity of all present and future rights to this
27# software under copyright law.
28#
29# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
30# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
31# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
32# IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
33# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
34# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
35# OTHER DEALINGS IN THE SOFTWARE.
36#
37# For more information, please refer to <http://unlicense.org/>
38
39"""
40YouCompleteMe configuration file tailored to support the 'meson' build system
41used by the 'systemd' project.
42"""
43
44import glob
45import os
46import ycm_core
47
48
49SOURCE_EXTENSIONS = (".C", ".cpp", ".cxx", ".cc", ".c", ".m", ".mm")
50HEADER_EXTENSIONS = (".H", ".h", ".hxx", ".hpp", ".hh")
51
52
53def DirectoryOfThisScript():
54    """
55    Return the absolute path of the parent directory containing this
56    script.
57    """
58    return os.path.dirname(os.path.abspath(__file__))
59
60
61def GuessBuildDirectory():
62    """
63    Guess the build directory using the following heuristics:
64
65    1. Returns the current directory of this script plus 'build'
66    subdirectory in absolute path if this subdirectory exists.
67
68    2. Otherwise, probes whether there exists any directory
69    containing '.ninja_log' file two levels above the current directory;
70    returns this single directory only if there is one candidate.
71    """
72    result = os.path.join(DirectoryOfThisScript(), "build")
73
74    if os.path.exists(result):
75        return result
76
77    result = glob.glob(os.path.join(DirectoryOfThisScript(),
78                                    "..", "..", "*", ".ninja_log"))
79
80    if not result:
81        return ""
82
83    if 1 != len(result):
84        return ""
85
86    return os.path.split(result[0])[0]
87
88
89def TraverseByDepth(root, include_extensions):
90    """
91    Return a set of child directories of the 'root' containing file
92    extensions specified in 'include_extensions'.
93
94    NOTE:
95        1. The 'root' directory itself is excluded from the result set.
96        2. No subdirectories would be excluded if 'include_extensions' is left
97           to 'None'.
98        3. Each entry in 'include_extensions' must begin with string '.'.
99    """
100    is_root = True
101    result = set()
102    # Perform a depth first top down traverse of the given directory tree.
103    for root_dir, subdirs, file_list in os.walk(root):
104        if not is_root:
105            # print("Relative Root: ", root_dir)
106            # print(subdirs)
107            if include_extensions:
108                get_ext = os.path.splitext
109                subdir_extensions = {
110                    get_ext(f)[-1] for f in file_list if get_ext(f)[-1]
111                }
112                if subdir_extensions & include_extensions:
113                    result.add(root_dir)
114            else:
115                result.add(root_dir)
116        else:
117            is_root = False
118
119    return result
120
121
122_project_src_dir = os.path.join(DirectoryOfThisScript(), "src")
123_include_dirs_set = TraverseByDepth(_project_src_dir, frozenset({".h"}))
124flags = [
125    "-x",
126    "c"
127    # The following flags are partially redundant due to the existence of
128    # 'compile_commands.json'.
129    #    '-Wall',
130    #    '-Wextra',
131    #    '-Wfloat-equal',
132    #    '-Wpointer-arith',
133    #    '-Wshadow',
134    #    '-std=gnu99',
135]
136
137for include_dir in _include_dirs_set:
138    flags.append("-I" + include_dir)
139
140# Set this to the absolute path to the folder (NOT the file!) containing the
141# compile_commands.json file to use that instead of 'flags'. See here for
142# more details: http://clang.llvm.org/docs/JSONCompilationDatabase.html
143#
144# You can get CMake to generate this file for you by adding:
145#   set( CMAKE_EXPORT_COMPILE_COMMANDS 1 )
146# to your CMakeLists.txt file.
147#
148# Most projects will NOT need to set this to anything; you can just change the
149# 'flags' list of compilation flags. Notice that YCM itself uses that approach.
150compilation_database_folder = GuessBuildDirectory()
151
152if os.path.exists(compilation_database_folder):
153    database = ycm_core.CompilationDatabase(compilation_database_folder)
154else:
155    database = None
156
157
158def MakeRelativePathsInFlagsAbsolute(flags, working_directory):
159    """
160    Iterate through 'flags' and replace the relative paths prefixed by
161    '-isystem', '-I', '-iquote', '--sysroot=' with absolute paths
162    start with 'working_directory'.
163    """
164    if not working_directory:
165        return list(flags)
166    new_flags = []
167    make_next_absolute = False
168    path_flags = ["-isystem", "-I", "-iquote", "--sysroot="]
169    for flag in flags:
170        new_flag = flag
171
172        if make_next_absolute:
173            make_next_absolute = False
174            if not flag.startswith("/"):
175                new_flag = os.path.join(working_directory, flag)
176
177        for path_flag in path_flags:
178            if flag == path_flag:
179                make_next_absolute = True
180                break
181
182            if flag.startswith(path_flag):
183                path = flag[len(path_flag):]
184                new_flag = path_flag + os.path.join(working_directory, path)
185                break
186
187        if new_flag:
188            new_flags.append(new_flag)
189    return new_flags
190
191
192def IsHeaderFile(filename):
193    """
194    Check whether 'filename' is considered as a header file.
195    """
196    extension = os.path.splitext(filename)[1]
197    return extension in HEADER_EXTENSIONS
198
199
200def GetCompilationInfoForFile(filename):
201    """
202    Helper function to look up compilation info of 'filename' in the 'database'.
203    """
204    # The compilation_commands.json file generated by CMake does not have
205    # entries for header files. So we do our best by asking the db for flags for
206    # a corresponding source file, if any. If one exists, the flags for that
207    # file should be good enough.
208    if not database:
209        return None
210
211    if IsHeaderFile(filename):
212        basename = os.path.splitext(filename)[0]
213        for extension in SOURCE_EXTENSIONS:
214            replacement_file = basename + extension
215            if os.path.exists(replacement_file):
216                compilation_info = \
217                    database.GetCompilationInfoForFile(replacement_file)
218                if compilation_info.compiler_flags_:
219                    return compilation_info
220        return None
221    return database.GetCompilationInfoForFile(filename)
222
223
224def FlagsForFile(filename, **kwargs):
225    """
226    Callback function to be invoked by YouCompleteMe in order to get the
227    information necessary to compile 'filename'.
228
229    It returns a dictionary with a single element 'flags'. This element is a
230    list of compiler flags to pass to libclang for the file 'filename'.
231    """
232    if database:
233        # Bear in mind that compilation_info.compiler_flags_ does NOT return a
234        # python list, but a "list-like" StringVec object
235        compilation_info = GetCompilationInfoForFile(filename)
236        if not compilation_info:
237            return None
238
239        final_flags = MakeRelativePathsInFlagsAbsolute(
240            compilation_info.compiler_flags_,
241            compilation_info.compiler_working_dir_)
242
243    else:
244        relative_to = DirectoryOfThisScript()
245        final_flags = MakeRelativePathsInFlagsAbsolute(flags, relative_to)
246
247    return {
248        "flags": final_flags,
249        "do_cache": True
250    }
251