blob: b8727d9633be4469626b0b953cec0f3a2733eeda [file] [log] [blame]
deadbeefdc9200e2017-03-02 21:48:39 -08001# Copyright (c) 2017 The WebRTC project authors. All Rights Reserved.
2#
3# Use of this source code is governed by a BSD-style license
4# that can be found in the LICENSE file in the root of the source
5# tree. An additional intellectual property rights grant can be found
6# in the file PATENTS. All contributing project authors may
7# be found in the AUTHORS file in the root of the source tree.
8
9# Autocompletion config for YouCompleteMe in WebRTC. This is just copied from
10# tools/vim in chromium with very minor modifications.
11#
12# USAGE:
13#
14# 1. Install YCM [https://github.com/Valloric/YouCompleteMe]
15# (Googlers should check out [go/ycm])
16#
17# 2. Create a symbolic link to this file called .ycm_extra_conf.py in the
18# directory above your WebRTC checkout (i.e. next to your .gclient file).
19#
20# cd src
Henrik Kjellander90fd7d82017-05-09 08:30:10 +020021# ln -rs tools_webrtc/vim/webrtc.ycm_extra_conf.py \
deadbeefdc9200e2017-03-02 21:48:39 -080022# ../.ycm_extra_conf.py
23#
24# 3. (optional) Whitelist the .ycm_extra_conf.py from step #2 by adding the
25# following to your .vimrc:
26#
27# let g:ycm_extra_conf_globlist=['<path to .ycm_extra_conf.py>']
28#
29# You can also add other .ycm_extra_conf.py files you want to use to this
30# list to prevent excessive prompting each time you visit a directory
31# covered by a config file.
32#
33# 4. Profit
34#
35#
36# Usage notes:
37#
38# * You must use ninja & clang to build WebRTC.
39#
40# * You must have run "gn gen" and built WebRTC recently.
41#
42#
43# Hacking notes:
44#
45# * The purpose of this script is to construct an accurate enough command line
46# for YCM to pass to clang so it can build and extract the symbols.
47#
48# * Right now, we only pull the -I and -D flags. That seems to be sufficient
49# for everything I've used it for.
50#
51# * That whole ninja & clang thing? We could support other configs if someone
52# were willing to write the correct commands and a parser.
53#
54# * This has only been tested on gPrecise.
55
deadbeefdc9200e2017-03-02 21:48:39 -080056import os
57import os.path
58import shlex
59import subprocess
60import sys
61
62# Flags from YCM's default config.
kjellanderdd460e22017-04-12 12:06:13 -070063_DEFAULT_FLAGS = [
Mirko Bonadei8cc66952020-10-30 10:13:45 +010064 '-DUSE_CLANG_COMPLETER',
65 '-std=c++11',
66 '-x',
67 'c++',
deadbeefdc9200e2017-03-02 21:48:39 -080068]
69
kjellanderdd460e22017-04-12 12:06:13 -070070_HEADER_ALTERNATES = ('.cc', '.cpp', '.c', '.mm', '.m')
deadbeefdc9200e2017-03-02 21:48:39 -080071
kjellanderdd460e22017-04-12 12:06:13 -070072_EXTENSION_FLAGS = {
Mirko Bonadei8cc66952020-10-30 10:13:45 +010073 '.m': ['-x', 'objective-c'],
74 '.mm': ['-x', 'objective-c++'],
deadbeefdc9200e2017-03-02 21:48:39 -080075}
76
Mirko Bonadei8cc66952020-10-30 10:13:45 +010077
deadbeefdc9200e2017-03-02 21:48:39 -080078def PathExists(*args):
Mirko Bonadei8cc66952020-10-30 10:13:45 +010079 return os.path.exists(os.path.join(*args))
deadbeefdc9200e2017-03-02 21:48:39 -080080
81
82def FindWebrtcSrcFromFilename(filename):
Mirko Bonadei8cc66952020-10-30 10:13:45 +010083 """Searches for the root of the WebRTC checkout.
deadbeefdc9200e2017-03-02 21:48:39 -080084
85 Simply checks parent directories until it finds .gclient and src/.
86
87 Args:
88 filename: (String) Path to source file being edited.
89
90 Returns:
91 (String) Path of 'src/', or None if unable to find.
92 """
Mirko Bonadei8cc66952020-10-30 10:13:45 +010093 curdir = os.path.normpath(os.path.dirname(filename))
94 while not (os.path.basename(curdir) == 'src'
95 and PathExists(curdir, 'DEPS') and
96 (PathExists(curdir, '..', '.gclient')
97 or PathExists(curdir, '.git'))):
98 nextdir = os.path.normpath(os.path.join(curdir, '..'))
99 if nextdir == curdir:
100 return None
101 curdir = nextdir
102 return curdir
deadbeefdc9200e2017-03-02 21:48:39 -0800103
104
105def GetDefaultSourceFile(webrtc_root, filename):
Mirko Bonadei8cc66952020-10-30 10:13:45 +0100106 """Returns the default source file to use as an alternative to |filename|.
deadbeefdc9200e2017-03-02 21:48:39 -0800107
108 Compile flags used to build the default source file is assumed to be a
109 close-enough approximation for building |filename|.
110
111 Args:
112 webrtc_root: (String) Absolute path to the root of WebRTC checkout.
113 filename: (String) Absolute path to the source file.
114
115 Returns:
116 (String) Absolute path to substitute source file.
117 """
Mirko Bonadei8cc66952020-10-30 10:13:45 +0100118 if 'test.' in filename:
119 return os.path.join(webrtc_root, 'base', 'logging_unittest.cc')
120 return os.path.join(webrtc_root, 'base', 'logging.cc')
deadbeefdc9200e2017-03-02 21:48:39 -0800121
122
123def GetNinjaBuildOutputsForSourceFile(out_dir, filename):
Mirko Bonadei8cc66952020-10-30 10:13:45 +0100124 """Returns a list of build outputs for filename.
deadbeefdc9200e2017-03-02 21:48:39 -0800125
126 The list is generated by invoking 'ninja -t query' tool to retrieve a list of
127 inputs and outputs of |filename|. This list is then filtered to only include
128 .o and .obj outputs.
129
130 Args:
131 out_dir: (String) Absolute path to ninja build output directory.
132 filename: (String) Absolute path to source file.
133
134 Returns:
135 (List of Strings) List of target names. Will return [] if |filename| doesn't
136 yield any .o or .obj outputs.
137 """
Mirko Bonadei8cc66952020-10-30 10:13:45 +0100138 # Ninja needs the path to the source file relative to the output build
139 # directory.
140 rel_filename = os.path.relpath(filename, out_dir)
deadbeefdc9200e2017-03-02 21:48:39 -0800141
Mirko Bonadei8cc66952020-10-30 10:13:45 +0100142 p = subprocess.Popen(['ninja', '-C', out_dir, '-t', 'query', rel_filename],
143 stdout=subprocess.PIPE,
144 stderr=subprocess.STDOUT,
145 universal_newlines=True)
146 stdout, _ = p.communicate()
147 if p.returncode != 0:
148 return []
deadbeefdc9200e2017-03-02 21:48:39 -0800149
Mirko Bonadei8cc66952020-10-30 10:13:45 +0100150 # The output looks like:
151 # ../../relative/path/to/source.cc:
152 # outputs:
153 # obj/reative/path/to/target.source.o
154 # obj/some/other/target2.source.o
155 # another/target.txt
156 #
157 outputs_text = stdout.partition('\n outputs:\n')[2]
158 output_lines = [line.strip() for line in outputs_text.split('\n')]
159 return [
160 target for target in output_lines
161 if target and (target.endswith('.o') or target.endswith('.obj'))
162 ]
deadbeefdc9200e2017-03-02 21:48:39 -0800163
164
165def GetClangCommandLineForNinjaOutput(out_dir, build_target):
Mirko Bonadei8cc66952020-10-30 10:13:45 +0100166 """Returns the Clang command line for building |build_target|
deadbeefdc9200e2017-03-02 21:48:39 -0800167
168 Asks ninja for the list of commands used to build |filename| and returns the
169 final Clang invocation.
170
171 Args:
172 out_dir: (String) Absolute path to ninja build output directory.
173 build_target: (String) A build target understood by ninja
174
175 Returns:
176 (String or None) Clang command line or None if a Clang command line couldn't
177 be determined.
178 """
Mirko Bonadei8cc66952020-10-30 10:13:45 +0100179 p = subprocess.Popen(
180 ['ninja', '-v', '-C', out_dir, '-t', 'commands', build_target],
181 stdout=subprocess.PIPE,
182 universal_newlines=True)
183 stdout, _ = p.communicate()
184 if p.returncode != 0:
185 return None
deadbeefdc9200e2017-03-02 21:48:39 -0800186
Mirko Bonadei8cc66952020-10-30 10:13:45 +0100187 # Ninja will return multiple build steps for all dependencies up to
188 # |build_target|. The build step we want is the last Clang invocation, which
189 # is expected to be the one that outputs |build_target|.
190 for line in reversed(stdout.split('\n')):
191 if 'clang' in line:
192 return line
193 return None
deadbeefdc9200e2017-03-02 21:48:39 -0800194
195
196def GetClangCommandLineFromNinjaForSource(out_dir, filename):
Mirko Bonadei8cc66952020-10-30 10:13:45 +0100197 """Returns a Clang command line used to build |filename|.
deadbeefdc9200e2017-03-02 21:48:39 -0800198
199 The same source file could be built multiple times using different tool
200 chains. In such cases, this command returns the first Clang invocation. We
201 currently don't prefer one toolchain over another. Hopefully the tool chain
202 corresponding to the Clang command line is compatible with the Clang build
203 used by YCM.
204
205 Args:
206 out_dir: (String) Absolute path to WebRTC checkout.
207 filename: (String) Absolute path to source file.
208
209 Returns:
210 (String or None): Command line for Clang invocation using |filename| as a
211 source. Returns None if no such command line could be found.
212 """
Mirko Bonadei8cc66952020-10-30 10:13:45 +0100213 build_targets = GetNinjaBuildOutputsForSourceFile(out_dir, filename)
214 for build_target in build_targets:
215 command_line = GetClangCommandLineForNinjaOutput(out_dir, build_target)
216 if command_line:
217 return command_line
218 return None
deadbeefdc9200e2017-03-02 21:48:39 -0800219
220
221def GetClangOptionsFromCommandLine(clang_commandline, out_dir,
222 additional_flags):
Mirko Bonadei8cc66952020-10-30 10:13:45 +0100223 """Extracts relevant command line options from |clang_commandline|
deadbeefdc9200e2017-03-02 21:48:39 -0800224
225 Args:
226 clang_commandline: (String) Full Clang invocation.
227 out_dir: (String) Absolute path to ninja build directory. Relative paths in
228 the command line are relative to |out_dir|.
229 additional_flags: (List of String) Additional flags to return.
230
231 Returns:
232 (List of Strings) The list of command line flags for this source file. Can
233 be empty.
234 """
Mirko Bonadei8cc66952020-10-30 10:13:45 +0100235 clang_flags = [] + additional_flags
deadbeefdc9200e2017-03-02 21:48:39 -0800236
Mirko Bonadei8cc66952020-10-30 10:13:45 +0100237 # Parse flags that are important for YCM's purposes.
238 clang_tokens = shlex.split(clang_commandline)
239 for flag_index, flag in enumerate(clang_tokens):
240 if flag.startswith('-I'):
241 # Relative paths need to be resolved, because they're relative to the
242 # output dir, not the source.
243 if flag[2] == '/':
244 clang_flags.append(flag)
245 else:
246 abs_path = os.path.normpath(os.path.join(out_dir, flag[2:]))
247 clang_flags.append('-I' + abs_path)
248 elif flag.startswith('-std'):
249 clang_flags.append(flag)
250 elif flag.startswith('-') and flag[1] in 'DWFfmO':
251 if flag == '-Wno-deprecated-register' or flag == '-Wno-header-guard':
252 # These flags causes libclang (3.3) to crash. Remove it until things
253 # are fixed.
254 continue
255 clang_flags.append(flag)
256 elif flag == '-isysroot':
257 # On Mac -isysroot <path> is used to find the system headers.
258 # Copy over both flags.
259 if flag_index + 1 < len(clang_tokens):
260 clang_flags.append(flag)
261 clang_flags.append(clang_tokens[flag_index + 1])
262 elif flag.startswith('--sysroot='):
263 # On Linux we use a sysroot image.
264 sysroot_path = flag.lstrip('--sysroot=')
265 if sysroot_path.startswith('/'):
266 clang_flags.append(flag)
267 else:
268 abs_path = os.path.normpath(os.path.join(
269 out_dir, sysroot_path))
270 clang_flags.append('--sysroot=' + abs_path)
271 return clang_flags
deadbeefdc9200e2017-03-02 21:48:39 -0800272
273
274def GetClangOptionsFromNinjaForFilename(webrtc_root, filename):
Mirko Bonadei8cc66952020-10-30 10:13:45 +0100275 """Returns the Clang command line options needed for building |filename|.
deadbeefdc9200e2017-03-02 21:48:39 -0800276
277 Command line options are based on the command used by ninja for building
278 |filename|. If |filename| is a .h file, uses its companion .cc or .cpp file.
279 If a suitable companion file can't be located or if ninja doesn't know about
280 |filename|, then uses default source files in WebRTC for determining the
281 commandline.
282
283 Args:
284 webrtc_root: (String) Path to src/.
285 filename: (String) Absolute path to source file being edited.
286
287 Returns:
288 (List of Strings) The list of command line flags for this source file. Can
289 be empty.
290 """
Mirko Bonadei8cc66952020-10-30 10:13:45 +0100291 if not webrtc_root:
292 return []
deadbeefdc9200e2017-03-02 21:48:39 -0800293
Mirko Bonadei8cc66952020-10-30 10:13:45 +0100294 # Generally, everyone benefits from including WebRTC's src/, because all of
295 # WebRTC's includes are relative to that.
296 additional_flags = ['-I' + os.path.join(webrtc_root)]
deadbeefdc9200e2017-03-02 21:48:39 -0800297
Mirko Bonadei8cc66952020-10-30 10:13:45 +0100298 # Version of Clang used to compile WebRTC can be newer then version of
299 # libclang that YCM uses for completion. So it's possible that YCM's libclang
300 # doesn't know about some used warning options, which causes compilation
301 # warnings (and errors, because of '-Werror');
302 additional_flags.append('-Wno-unknown-warning-option')
deadbeefdc9200e2017-03-02 21:48:39 -0800303
Mirko Bonadei8cc66952020-10-30 10:13:45 +0100304 sys.path.append(os.path.join(webrtc_root, 'tools', 'vim'))
305 from ninja_output import GetNinjaOutputDirectory
306 out_dir = GetNinjaOutputDirectory(webrtc_root)
deadbeefdc9200e2017-03-02 21:48:39 -0800307
Mirko Bonadei8cc66952020-10-30 10:13:45 +0100308 basename, extension = os.path.splitext(filename)
309 if extension == '.h':
310 candidates = [basename + ext for ext in _HEADER_ALTERNATES]
311 else:
312 candidates = [filename]
deadbeefdc9200e2017-03-02 21:48:39 -0800313
Mirko Bonadei8cc66952020-10-30 10:13:45 +0100314 clang_line = None
315 buildable_extension = extension
316 for candidate in candidates:
317 clang_line = GetClangCommandLineFromNinjaForSource(out_dir, candidate)
318 if clang_line:
319 buildable_extension = os.path.splitext(candidate)[1]
320 break
deadbeefdc9200e2017-03-02 21:48:39 -0800321
Mirko Bonadei8cc66952020-10-30 10:13:45 +0100322 additional_flags += _EXTENSION_FLAGS.get(buildable_extension, [])
deadbeefdc9200e2017-03-02 21:48:39 -0800323
Mirko Bonadei8cc66952020-10-30 10:13:45 +0100324 if not clang_line:
325 # If ninja didn't know about filename or it's companion files, then try a
326 # default build target. It is possible that the file is new, or build.ninja
327 # is stale.
328 clang_line = GetClangCommandLineFromNinjaForSource(
329 out_dir, GetDefaultSourceFile(webrtc_root, filename))
deadbeefdc9200e2017-03-02 21:48:39 -0800330
Mirko Bonadei8cc66952020-10-30 10:13:45 +0100331 if not clang_line:
332 return additional_flags
deadbeefdc9200e2017-03-02 21:48:39 -0800333
Mirko Bonadei8cc66952020-10-30 10:13:45 +0100334 return GetClangOptionsFromCommandLine(clang_line, out_dir,
335 additional_flags)
deadbeefdc9200e2017-03-02 21:48:39 -0800336
337
338def FlagsForFile(filename):
Mirko Bonadei8cc66952020-10-30 10:13:45 +0100339 """This is the main entry point for YCM. Its interface is fixed.
deadbeefdc9200e2017-03-02 21:48:39 -0800340
341 Args:
342 filename: (String) Path to source file being edited.
343
344 Returns:
345 (Dictionary)
346 'flags': (List of Strings) Command line flags.
347 'do_cache': (Boolean) True if the result should be cached.
348 """
Mirko Bonadei8cc66952020-10-30 10:13:45 +0100349 abs_filename = os.path.abspath(filename)
350 webrtc_root = FindWebrtcSrcFromFilename(abs_filename)
351 clang_flags = GetClangOptionsFromNinjaForFilename(webrtc_root,
352 abs_filename)
deadbeefdc9200e2017-03-02 21:48:39 -0800353
Mirko Bonadei8cc66952020-10-30 10:13:45 +0100354 # If clang_flags could not be determined, then assume that was due to a
355 # transient failure. Preventing YCM from caching the flags allows us to try to
356 # determine the flags again.
357 should_cache_flags_for_file = bool(clang_flags)
deadbeefdc9200e2017-03-02 21:48:39 -0800358
Mirko Bonadei8cc66952020-10-30 10:13:45 +0100359 final_flags = _DEFAULT_FLAGS + clang_flags
deadbeefdc9200e2017-03-02 21:48:39 -0800360
Mirko Bonadei8cc66952020-10-30 10:13:45 +0100361 return {'flags': final_flags, 'do_cache': should_cache_flags_for_file}