1# This Source Code Form is subject to the terms of the Mozilla Public 2# License, v. 2.0. If a copy of the MPL was not distributed with this file, 3# You can obtain one at http://mozilla.org/MPL/2.0/. 4 5# This modules provides functionality for dealing with code completion. 6 7from __future__ import absolute_import, print_function 8 9import os 10 11from mozbuild.backend.common import CommonBackend 12from mozbuild.frontend.data import ( 13 ComputedFlags, 14 Sources, 15 DirectoryTraversal, 16 PerSourceFlag, 17 VariablePassthru, 18) 19from mozbuild.shellutil import quote as shell_quote 20from mozbuild.util import expand_variables 21import mozpack.path as mozpath 22from collections import ( 23 defaultdict, 24 OrderedDict, 25) 26 27 28class CompileDBBackend(CommonBackend): 29 def _init(self): 30 CommonBackend._init(self) 31 32 # The database we're going to dump out to. 33 self._db = OrderedDict() 34 35 # The cache for per-directory flags 36 self._flags = {} 37 38 self._envs = {} 39 self._local_flags = defaultdict(dict) 40 self._per_source_flags = defaultdict(list) 41 42 def _build_cmd(self, cmd, filename, unified): 43 cmd = list(cmd) 44 if unified is None: 45 cmd.append(filename) 46 else: 47 cmd.append(unified) 48 49 return cmd 50 51 def consume_object(self, obj): 52 # Those are difficult directories, that will be handled later. 53 if obj.relsrcdir in ( 54 "build/unix/elfhack", 55 "build/unix/elfhack/inject", 56 "build/clang-plugin", 57 "build/clang-plugin/tests", 58 ): 59 return True 60 61 consumed = CommonBackend.consume_object(self, obj) 62 63 if consumed: 64 return True 65 66 if isinstance(obj, DirectoryTraversal): 67 self._envs[obj.objdir] = obj.config 68 69 elif isinstance(obj, Sources): 70 # For other sources, include each source file. 71 for f in obj.files: 72 self._build_db_line( 73 obj.objdir, obj.relsrcdir, obj.config, f, obj.canonical_suffix 74 ) 75 76 elif isinstance(obj, VariablePassthru): 77 for var in ("MOZBUILD_CMFLAGS", "MOZBUILD_CMMFLAGS"): 78 if var in obj.variables: 79 self._local_flags[obj.objdir][var] = obj.variables[var] 80 81 elif isinstance(obj, PerSourceFlag): 82 self._per_source_flags[obj.file_name].extend(obj.flags) 83 84 elif isinstance(obj, ComputedFlags): 85 for var, flags in obj.get_flags(): 86 self._local_flags[obj.objdir]["COMPUTED_%s" % var] = flags 87 88 return True 89 90 def consume_finished(self): 91 CommonBackend.consume_finished(self) 92 93 db = [] 94 95 for (directory, filename, unified), cmd in self._db.items(): 96 env = self._envs[directory] 97 cmd = self._build_cmd(cmd, filename, unified) 98 variables = { 99 "DIST": mozpath.join(env.topobjdir, "dist"), 100 "DEPTH": env.topobjdir, 101 "MOZILLA_DIR": env.topsrcdir, 102 "topsrcdir": env.topsrcdir, 103 "topobjdir": env.topobjdir, 104 } 105 variables.update(self._local_flags[directory]) 106 c = [] 107 for a in cmd: 108 accum = "" 109 for word in expand_variables(a, variables).split(): 110 # We can't just split() the output of expand_variables since 111 # there can be spaces enclosed by quotes, e.g. '"foo bar"'. 112 # Handle that case by checking whether there are an even 113 # number of double-quotes in the word and appending it to 114 # the accumulator if not. Meanwhile, shlex.split() and 115 # mozbuild.shellutil.split() aren't able to properly handle 116 # this and break in various ways, so we can't use something 117 # off-the-shelf. 118 has_quote = bool(word.count('"') % 2) 119 if accum and has_quote: 120 c.append(accum + " " + word) 121 accum = "" 122 elif accum and not has_quote: 123 accum += " " + word 124 elif not accum and has_quote: 125 accum = word 126 else: 127 c.append(word) 128 # Tell clangd to keep parsing to the end of a file, regardless of 129 # how many errors are encountered. (Unified builds mean that we 130 # encounter a lot of errors parsing some files.) 131 c.insert(-1, "-ferror-limit=0") 132 133 per_source_flags = self._per_source_flags.get(filename) 134 if per_source_flags is not None: 135 c.extend(per_source_flags) 136 db.append( 137 { 138 "directory": directory, 139 "command": " ".join(shell_quote(a) for a in c), 140 "file": mozpath.join(directory, filename), 141 } 142 ) 143 144 import json 145 146 outputfile = self._outputfile_path() 147 with self._write_file(outputfile) as jsonout: 148 json.dump(db, jsonout, indent=0) 149 150 def _outputfile_path(self): 151 # Output the database (a JSON file) to objdir/compile_commands.json 152 return os.path.join(self.environment.topobjdir, "compile_commands.json") 153 154 def _process_unified_sources(self, obj): 155 if not obj.have_unified_mapping: 156 for f in list(sorted(obj.files)): 157 self._build_db_line( 158 obj.objdir, obj.relsrcdir, obj.config, f, obj.canonical_suffix 159 ) 160 return 161 162 # For unified sources, only include the unified source file. 163 # Note that unified sources are never used for host sources. 164 for f in obj.unified_source_mapping: 165 self._build_db_line( 166 obj.objdir, obj.relsrcdir, obj.config, f[0], obj.canonical_suffix 167 ) 168 for entry in f[1]: 169 self._build_db_line( 170 obj.objdir, 171 obj.relsrcdir, 172 obj.config, 173 entry, 174 obj.canonical_suffix, 175 unified=f[0], 176 ) 177 178 def _handle_idl_manager(self, idl_manager): 179 pass 180 181 def _handle_ipdl_sources( 182 self, 183 ipdl_dir, 184 sorted_ipdl_sources, 185 sorted_nonstatic_ipdl_sources, 186 sorted_static_ipdl_sources, 187 ): 188 pass 189 190 def _handle_webidl_build( 191 self, 192 bindings_dir, 193 unified_source_mapping, 194 webidls, 195 expected_build_output_files, 196 global_define_files, 197 ): 198 for f in unified_source_mapping: 199 self._build_db_line(bindings_dir, None, self.environment, f[0], ".cpp") 200 201 COMPILERS = { 202 ".c": "CC", 203 ".cpp": "CXX", 204 ".m": "CC", 205 ".mm": "CXX", 206 } 207 208 CFLAGS = { 209 ".c": "CFLAGS", 210 ".cpp": "CXXFLAGS", 211 ".m": "CFLAGS", 212 ".mm": "CXXFLAGS", 213 } 214 215 def _get_compiler_args(self, cenv, canonical_suffix): 216 if canonical_suffix not in self.COMPILERS: 217 return None 218 return cenv.substs[self.COMPILERS[canonical_suffix]].split() 219 220 def _build_db_line( 221 self, objdir, reldir, cenv, filename, canonical_suffix, unified=None 222 ): 223 compiler_args = self._get_compiler_args(cenv, canonical_suffix) 224 if compiler_args is None: 225 return 226 db = self._db.setdefault( 227 (objdir, filename, unified), 228 compiler_args + ["-o", "/dev/null", "-c"], 229 ) 230 reldir = reldir or mozpath.relpath(objdir, cenv.topobjdir) 231 232 def append_var(name): 233 value = cenv.substs.get(name) 234 if not value: 235 return 236 if isinstance(value, str): 237 value = value.split() 238 db.extend(value) 239 240 db.append("$(COMPUTED_%s)" % self.CFLAGS[canonical_suffix]) 241 if canonical_suffix == ".m": 242 append_var("OS_COMPILE_CMFLAGS") 243 db.append("$(MOZBUILD_CMFLAGS)") 244 elif canonical_suffix == ".mm": 245 append_var("OS_COMPILE_CMMFLAGS") 246 db.append("$(MOZBUILD_CMMFLAGS)") 247