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