• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# 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
21#          ln -rs tools_webrtc/vim/webrtc.ycm_extra_conf.py \
22#          ../.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
56
57import os
58import os.path
59import shlex
60import subprocess
61import sys
62
63# Flags from YCM's default config.
64_DEFAULT_FLAGS = [
65  '-DUSE_CLANG_COMPLETER',
66  '-std=c++11',
67  '-x',
68  'c++',
69]
70
71_HEADER_ALTERNATES = ('.cc', '.cpp', '.c', '.mm', '.m')
72
73_EXTENSION_FLAGS = {
74  '.m': ['-x', 'objective-c'],
75  '.mm': ['-x', 'objective-c++'],
76}
77
78def PathExists(*args):
79  return os.path.exists(os.path.join(*args))
80
81
82def FindWebrtcSrcFromFilename(filename):
83  """Searches for the root of the WebRTC checkout.
84
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  """
93  curdir = os.path.normpath(os.path.dirname(filename))
94  while not (os.path.basename(curdir) == 'src'
95             and PathExists(curdir, 'DEPS')
96             and (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
103
104
105def GetDefaultSourceFile(webrtc_root, filename):
106  """Returns the default source file to use as an alternative to |filename|.
107
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  """
118  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')
121
122
123def GetNinjaBuildOutputsForSourceFile(out_dir, filename):
124  """Returns a list of build outputs for filename.
125
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  """
138  # Ninja needs the path to the source file relative to the output build
139  # directory.
140  rel_filename = os.path.relpath(filename, out_dir)
141
142  p = subprocess.Popen(['ninja', '-C', out_dir, '-t', 'query', rel_filename],
143                       stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
144                       universal_newlines=True)
145  stdout, _ = p.communicate()
146  if p.returncode != 0:
147    return []
148
149  # The output looks like:
150  #   ../../relative/path/to/source.cc:
151  #     outputs:
152  #       obj/reative/path/to/target.source.o
153  #       obj/some/other/target2.source.o
154  #       another/target.txt
155  #
156  outputs_text = stdout.partition('\n  outputs:\n')[2]
157  output_lines = [line.strip() for line in outputs_text.split('\n')]
158  return [target for target in output_lines
159          if target and (target.endswith('.o') or target.endswith('.obj'))]
160
161
162def GetClangCommandLineForNinjaOutput(out_dir, build_target):
163  """Returns the Clang command line for building |build_target|
164
165  Asks ninja for the list of commands used to build |filename| and returns the
166  final Clang invocation.
167
168  Args:
169    out_dir: (String) Absolute path to ninja build output directory.
170    build_target: (String) A build target understood by ninja
171
172  Returns:
173    (String or None) Clang command line or None if a Clang command line couldn't
174        be determined.
175  """
176  p = subprocess.Popen(['ninja', '-v', '-C', out_dir,
177                        '-t', 'commands', build_target],
178                       stdout=subprocess.PIPE, universal_newlines=True)
179  stdout, _ = p.communicate()
180  if p.returncode != 0:
181    return None
182
183  # Ninja will return multiple build steps for all dependencies up to
184  # |build_target|. The build step we want is the last Clang invocation, which
185  # is expected to be the one that outputs |build_target|.
186  for line in reversed(stdout.split('\n')):
187    if 'clang' in line:
188      return line
189  return None
190
191
192def GetClangCommandLineFromNinjaForSource(out_dir, filename):
193  """Returns a Clang command line used to build |filename|.
194
195  The same source file could be built multiple times using different tool
196  chains. In such cases, this command returns the first Clang invocation. We
197  currently don't prefer one toolchain over another. Hopefully the tool chain
198  corresponding to the Clang command line is compatible with the Clang build
199  used by YCM.
200
201  Args:
202    out_dir: (String) Absolute path to WebRTC checkout.
203    filename: (String) Absolute path to source file.
204
205  Returns:
206    (String or None): Command line for Clang invocation using |filename| as a
207        source. Returns None if no such command line could be found.
208  """
209  build_targets = GetNinjaBuildOutputsForSourceFile(out_dir, filename)
210  for build_target in build_targets:
211    command_line = GetClangCommandLineForNinjaOutput(out_dir, build_target)
212    if command_line:
213      return command_line
214  return None
215
216
217def GetClangOptionsFromCommandLine(clang_commandline, out_dir,
218                                   additional_flags):
219  """Extracts relevant command line options from |clang_commandline|
220
221  Args:
222    clang_commandline: (String) Full Clang invocation.
223    out_dir: (String) Absolute path to ninja build directory. Relative paths in
224        the command line are relative to |out_dir|.
225    additional_flags: (List of String) Additional flags to return.
226
227  Returns:
228    (List of Strings) The list of command line flags for this source file. Can
229    be empty.
230  """
231  clang_flags = [] + additional_flags
232
233  # Parse flags that are important for YCM's purposes.
234  clang_tokens = shlex.split(clang_commandline)
235  for flag_index, flag in enumerate(clang_tokens):
236    if flag.startswith('-I'):
237      # Relative paths need to be resolved, because they're relative to the
238      # output dir, not the source.
239      if flag[2] == '/':
240        clang_flags.append(flag)
241      else:
242        abs_path = os.path.normpath(os.path.join(out_dir, flag[2:]))
243        clang_flags.append('-I' + abs_path)
244    elif flag.startswith('-std'):
245      clang_flags.append(flag)
246    elif flag.startswith('-') and flag[1] in 'DWFfmO':
247      if flag == '-Wno-deprecated-register' or flag == '-Wno-header-guard':
248        # These flags causes libclang (3.3) to crash. Remove it until things
249        # are fixed.
250        continue
251      clang_flags.append(flag)
252    elif flag == '-isysroot':
253      # On Mac -isysroot <path> is used to find the system headers.
254      # Copy over both flags.
255      if flag_index + 1 < len(clang_tokens):
256        clang_flags.append(flag)
257        clang_flags.append(clang_tokens[flag_index + 1])
258    elif flag.startswith('--sysroot='):
259      # On Linux we use a sysroot image.
260      sysroot_path = flag.lstrip('--sysroot=')
261      if sysroot_path.startswith('/'):
262        clang_flags.append(flag)
263      else:
264        abs_path = os.path.normpath(os.path.join(out_dir, sysroot_path))
265        clang_flags.append('--sysroot=' + abs_path)
266  return clang_flags
267
268
269def GetClangOptionsFromNinjaForFilename(webrtc_root, filename):
270  """Returns the Clang command line options needed for building |filename|.
271
272  Command line options are based on the command used by ninja for building
273  |filename|. If |filename| is a .h file, uses its companion .cc or .cpp file.
274  If a suitable companion file can't be located or if ninja doesn't know about
275  |filename|, then uses default source files in WebRTC for determining the
276  commandline.
277
278  Args:
279    webrtc_root: (String) Path to src/.
280    filename: (String) Absolute path to source file being edited.
281
282  Returns:
283    (List of Strings) The list of command line flags for this source file. Can
284    be empty.
285  """
286  if not webrtc_root:
287    return []
288
289  # Generally, everyone benefits from including WebRTC's src/, because all of
290  # WebRTC's includes are relative to that.
291  additional_flags = ['-I' + os.path.join(webrtc_root)]
292
293  # Version of Clang used to compile WebRTC can be newer then version of
294  # libclang that YCM uses for completion. So it's possible that YCM's libclang
295  # doesn't know about some used warning options, which causes compilation
296  # warnings (and errors, because of '-Werror');
297  additional_flags.append('-Wno-unknown-warning-option')
298
299  sys.path.append(os.path.join(webrtc_root, 'tools', 'vim'))
300  from ninja_output import GetNinjaOutputDirectory
301  out_dir = GetNinjaOutputDirectory(webrtc_root)
302
303  basename, extension = os.path.splitext(filename)
304  if extension == '.h':
305    candidates = [basename + ext for ext in _HEADER_ALTERNATES]
306  else:
307    candidates = [filename]
308
309  clang_line = None
310  buildable_extension = extension
311  for candidate in candidates:
312    clang_line = GetClangCommandLineFromNinjaForSource(out_dir, candidate)
313    if clang_line:
314      buildable_extension = os.path.splitext(candidate)[1]
315      break
316
317  additional_flags += _EXTENSION_FLAGS.get(buildable_extension, [])
318
319  if not clang_line:
320    # If ninja didn't know about filename or it's companion files, then try a
321    # default build target. It is possible that the file is new, or build.ninja
322    # is stale.
323    clang_line = GetClangCommandLineFromNinjaForSource(
324        out_dir, GetDefaultSourceFile(webrtc_root, filename))
325
326  if not clang_line:
327    return additional_flags
328
329  return GetClangOptionsFromCommandLine(clang_line, out_dir, additional_flags)
330
331
332def FlagsForFile(filename):
333  """This is the main entry point for YCM. Its interface is fixed.
334
335  Args:
336    filename: (String) Path to source file being edited.
337
338  Returns:
339    (Dictionary)
340      'flags': (List of Strings) Command line flags.
341      'do_cache': (Boolean) True if the result should be cached.
342  """
343  abs_filename = os.path.abspath(filename)
344  webrtc_root = FindWebrtcSrcFromFilename(abs_filename)
345  clang_flags = GetClangOptionsFromNinjaForFilename(webrtc_root, abs_filename)
346
347  # If clang_flags could not be determined, then assume that was due to a
348  # transient failure. Preventing YCM from caching the flags allows us to try to
349  # determine the flags again.
350  should_cache_flags_for_file = bool(clang_flags)
351
352  final_flags = _DEFAULT_FLAGS + clang_flags
353
354  return {
355    'flags': final_flags,
356    'do_cache': should_cache_flags_for_file
357  }
358