xref: /aosp_15_r20/external/pigweed/pw_toolchain/static_analysis_toolchain.gni (revision 61c4878ac05f98d0ceed94b57d316916de578985)
1# Copyright 2022 The Pigweed Authors
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may not
4# use this file except in compliance with the License. You may obtain a copy of
5# the License at
6#
7#     https://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations under
13# the License.
14
15import("//build_overrides/pigweed.gni")
16
17import("$dir_pw_compilation_testing/negative_compilation_test.gni")
18import("$dir_pw_third_party/boringssl/boringssl.gni")
19import("$dir_pw_third_party/chre/chre.gni")
20import("$dir_pw_third_party/googletest/googletest.gni")
21import("$dir_pw_third_party/mbedtls/mbedtls.gni")
22import("$dir_pw_toolchain/universal_tools.gni")
23
24declare_args() {
25  # Regular expressions matching the paths of the source files to be excluded
26  # from the analysis. clang-tidy provides no alternative option.
27  #
28  # For example, the following disables clang-tidy on all source files in the
29  # third_party directory:
30  #
31  #   pw_toolchain_STATIC_ANALYSIS_SKIP_SOURCES_RES = ["third_party/.*"]
32  #
33  pw_toolchain_STATIC_ANALYSIS_SKIP_SOURCES_RES = []
34
35  # Disable clang-tidy for specific include paths. In the clang-tidy command,
36  # include paths that end with one of these, or match as a regular expression,
37  # are switched from -I to -isystem, which causes clang-tidy to ignore them.
38  # Unfortunately, clang-tidy provides no other way to filter header files.
39  #
40  # For example, the following ignores header files in "repo/include":
41  #
42  #   pw_toolchain_STATIC_ANALYSIS_SKIP_INCLUDE_PATHS = ["repo/include"]
43  #
44  # While the following ignores all third-party header files:
45  #
46  #   pw_toolchain_STATIC_ANALYSIS_SKIP_INCLUDE_PATHS = [".*/third_party/.*"]
47  #
48  pw_toolchain_STATIC_ANALYSIS_SKIP_INCLUDE_PATHS = []
49}
50
51# Third-party software with Pigweed-supported build files that do not pass all
52# clang-tidy checks.
53_excluded_third_party_dirs = [
54  dir_pw_third_party_mbedtls,
55  dir_pw_third_party_boringssl,
56  dir_pw_third_party_googletest,
57  dir_pw_third_party_chre,
58]
59
60# Creates a toolchain target for static analysis.
61#
62# The generated toolchain runs clang-tidy on all source files that are not
63# excluded by pw_toolchain_STATIC_ANALYSIS_SKIP_SOURCES_RES or
64# pw_toolchain_STATIC_ANALYSIS_SKIP_INCLUDE_PATHS.
65#
66# Args:
67#   cc: (required) String indicating the C compiler to use.
68#   cxx: (required) String indicating the C++ compiler to use.
69#   static_analysis: (required) A scope defining args to apply to the
70#     static_analysis toolchain.
71#   static_analysis.enabled: (required) Bool used to indicate whether
72#       static_analysis should be enabled for the toolchain where scope is
73#       applied to. Note that static_analysis.enabled must be set in order to
74#       use this toolchain.
75#   static_analysis.clang_tidy_path: (optional) String indicating clang-tidy bin
76#       to use.
77#   static_analysis.cc_post: (optional) String defining additional commands to
78#       append to cc tool's command list (i.e command(s) to run after cc command
79#       chain).
80#   static_analysis.cxx_post: (optional) String defining additional commands to
81#       append to cxx tool's command list (i.e command(s) to run after cxx
82#       command chain).
83template("pw_static_analysis_toolchain") {
84  invoker_toolchain_args = invoker.defaults
85  assert(defined(invoker.static_analysis), "static_analysis scope missing.")
86  _static_analysis_args = invoker.static_analysis
87  assert(defined(_static_analysis_args.enabled),
88         "static_analysis.enabled is missing")
89  assert(_static_analysis_args.enabled,
90         "static_analysis.enabled must be true to use this toolchain.")
91
92  _skipped_regexps = []
93  _skipped_include_paths = []
94  foreach(third_party_dir, _excluded_third_party_dirs) {
95    if (third_party_dir != "") {
96      _skipped_include_paths += [
97        third_party_dir + "/include",
98        third_party_dir,
99      ]
100    }
101  }
102
103  _skipped_regexps += pw_toolchain_STATIC_ANALYSIS_SKIP_SOURCES_RES
104  _skipped_include_paths += pw_toolchain_STATIC_ANALYSIS_SKIP_INCLUDE_PATHS
105
106  # Clang tidy is invoked by a wrapper script which implements the missing
107  # option --source-filter.
108  _clang_tidy_py_path =
109      rebase_path("$dir_pw_toolchain/py/pw_toolchain/clang_tidy.py",
110                  root_build_dir)
111  _clang_tidy_py = "${python_path} ${_clang_tidy_py_path}"
112  _source_root = rebase_path("//", root_build_dir)
113  _source_exclude = ""
114  foreach(pattern, _skipped_regexps) {
115    _source_exclude = _source_exclude + " --source-exclude '${pattern}'"
116  }
117  _skip_include_path = ""
118  foreach(pattern, _skipped_include_paths) {
119    _skip_include_path =
120        _skip_include_path + " --skip-include-path '${pattern}'"
121  }
122  _clang_tidy_path = ""
123  if (defined(_static_analysis_args.clang_tidy_path)) {
124    _clang_tidy_path =
125        "--clang-tidy " +
126        rebase_path(_static_analysis_args.clang_tidy_path, root_build_dir)
127  }
128
129  toolchain(target_name) {
130    # Uncomment this line to see which toolchains generate other toolchains.
131    # print("Generating toolchain: ${target_name} by ${current_toolchain}")
132
133    tool("asm") {
134      depfile = "{{output}}.d"
135      command = pw_universal_stamp.command
136      depsformat = "gcc"
137      description = "as {{output}}"
138      outputs = [
139        # Use {{source_file_part}}, which includes the extension, instead of
140        # {{source_name_part}} so that object files created from <file_name>.c
141        # and <file_name>.cc sources are unique.
142        "{{source_out_dir}}/{{target_output_name}}.{{source_file_part}}.o",
143      ]
144    }
145
146    assert(defined(invoker.cc), "toolchain is missing 'cc'")
147    tool("cc") {
148      _post_command_hook = ""
149      if (defined(_static_analysis_args.cc_post) &&
150          _static_analysis_args.cc_post != "") {
151        _post_command_hook += " && " + _static_analysis_args.cc_post
152      }
153
154      depfile = "{{output}}.d"
155      command = string_join(" ",
156                            [
157                              _clang_tidy_py,
158                              _source_exclude,
159                              _skip_include_path,
160                              _clang_tidy_path,
161                              "--source-file {{source}}",
162                              "--source-root '${_source_root}'",
163                              "--export-fixes {{output}}.yaml",
164                              "--",
165                              invoker.cc,
166                              "END_OF_INVOKER",
167                              "-MMD -MF $depfile",  # Write out dependencies.
168                              "{{cflags}}",
169                              "{{cflags_c}}",  # Must come after {{cflags}}.
170                              "{{defines}}",
171                              "{{include_dirs}}",
172                              "-c {{source}}",
173                              "-o {{output}}",
174                            ]) + " && touch {{output}}" + _post_command_hook
175      depsformat = "gcc"
176      description = "clang-tidy {{source}}"
177      outputs =
178          [ "{{source_out_dir}}/{{target_output_name}}.{{source_file_part}}.o" ]
179    }
180
181    assert(defined(invoker.cxx), "toolchain is missing 'cxx'")
182    tool("cxx") {
183      _post_command_hook = ""
184      if (defined(_static_analysis_args.cxx_post) &&
185          _static_analysis_args.cxx_post != "") {
186        _post_command_hook += " && " + _static_analysis_args.cxx_post
187      }
188
189      depfile = "{{output}}.d"
190      command = string_join(" ",
191                            [
192                              _clang_tidy_py,
193                              _source_exclude,
194                              _skip_include_path,
195                              _clang_tidy_path,
196                              "--source-file {{source}}",
197                              "--source-root '${_source_root}'",
198                              "--export-fixes {{output}}.yaml",
199                              "--",
200                              invoker.cxx,
201                              "END_OF_INVOKER",
202                              "-MMD -MF $depfile",  # Write out dependencies.
203                              "{{cflags}}",
204                              "{{cflags_cc}}",  # Must come after {{cflags}}.
205                              "{{defines}}",
206                              "{{include_dirs}}",
207                              "-c {{source}}",
208                              "-o {{output}}",
209                            ]) + " && touch {{output}}" + _post_command_hook
210      depsformat = "gcc"
211      description = "clang-tidy {{source}}"
212      outputs =
213          [ "{{source_out_dir}}/{{target_output_name}}.{{source_file_part}}.o" ]
214    }
215
216    tool("objc") {
217      depfile = "{{output}}.d"
218      command = pw_universal_stamp.command
219      depsformat = "gcc"
220      description = "objc {{source}}"
221      outputs =
222          [ "{{source_out_dir}}/{{target_output_name}}.{{source_file_part}}.o" ]
223    }
224
225    tool("objcxx") {
226      depfile = "{{output}}.d"
227      command = pw_universal_stamp.command
228      depsformat = "gcc"
229      description = "objc++ {{output}}"
230      outputs =
231          [ "{{source_out_dir}}/{{target_output_name}}.{{source_file_part}}.o" ]
232    }
233
234    tool("alink") {
235      command = "rm -f {{output}} && touch {{output}}"
236      description = "ar {{target_output_name}}{{output_extension}}"
237      outputs = [ "{{output_dir}}/{{target_output_name}}{{output_extension}}" ]
238      default_output_extension = ".a"
239      default_output_dir = "{{target_out_dir}}/lib"
240    }
241
242    tool("link") {
243      if (host_os == "win") {
244        # Force the extension to '.bat', empty bat scripts are still
245        # executable and will not raise errors.
246        _output = "{{output_dir}}/{{target_output_name}}.bat"
247        command = pw_universal_stamp.command
248        default_output_extension = ".bat"
249      } else {
250        default_output_extension = ""
251        _output = "{{output_dir}}/{{target_output_name}}{{output_extension}}"
252        command = "touch {{output}} && chmod +x {{output}}"
253      }
254      description = "ld $_output"
255      outputs = [ _output ]
256      default_output_dir = "{{target_out_dir}}/bin"
257    }
258
259    tool("solink") {
260      _output = "{{output_dir}}/{{target_output_name}}{{output_extension}}"
261      command = pw_universal_stamp.command
262      description = "ld -shared $_output"
263      outputs = [ _output ]
264      default_output_dir = "{{target_out_dir}}/lib"
265      default_output_extension = ".so"
266    }
267
268    tool("stamp") {
269      # GN-ism: GN gets mad if you directly forward the contents of
270      # pw_universal_stamp.
271      _stamp = pw_universal_stamp
272      forward_variables_from(_stamp, "*")
273    }
274
275    tool("copy") {
276      # GN-ism: GN gets mad if you directly forward the contents of
277      # pw_universal_copy.
278      _copy = pw_universal_copy
279      forward_variables_from(_copy, "*")
280    }
281
282    # Build arguments to be overridden when compiling cross-toolchain:
283    #
284    #   pw_toolchain_defaults: A scope setting defaults to apply to GN targets
285    #     in this toolchain. It is analogous to $pw_target_defaults in
286    #     $dir_pigweed/pw_vars_default.gni.
287    #
288    #   pw_toolchain_SCOPE: A copy of the invoker scope that defines the
289    #     toolchain. Used for generating derivative toolchains.
290    #
291    toolchain_args = {
292      pw_toolchain_SCOPE = {
293      }
294      pw_toolchain_SCOPE = {
295        forward_variables_from(invoker, "*")
296        name = target_name
297      }
298      forward_variables_from(invoker_toolchain_args, "*")
299
300      # Disable compilation testing for static analysis toolchains.
301      pw_compilation_testing_NEGATIVE_COMPILATION_ENABLED = false
302
303      # Always disable coverage generation since we will not actually run the
304      # instrumented binaries to produce a profraw file.
305      pw_toolchain_COVERAGE_ENABLED = false
306    }
307
308    _generate_rust_tools = defined(invoker.rustc)
309    if (_generate_rust_tools) {
310      if (defined(invoker.ld)) {
311        _rustc_linker = "-Clinker=${invoker.ld}"
312      } else {
313        _rustc_linker = ""
314      }
315
316      _rustc_command = string_join(
317              " ",
318              [
319                # TODO: b/234872510 - Ensure this works with Windows.
320                "RUST_BACKTRACE=1",
321                "{{rustenv}}",
322                invoker.rustc,
323                "{{source}}",
324                "--crate-name {{crate_name}}",
325                "--crate-type {{crate_type}}",
326                _rustc_linker,
327                "{{externs}}",
328                "{{rustdeps}}",
329                "{{rustflags}}",
330                "-D warnings",
331                "--color always",
332                "--emit=dep-info={{output}}.d,link",
333                "-o {{output_dir}}/{{target_output_name}}{{output_extension}}",
334              ])
335
336      _output = "{{output_dir}}/{{target_output_name}}{{output_extension}}"
337
338      tool("rust_bin") {
339        description = "rustc {{output}}"
340        default_output_dir = "{{target_out_dir}}/bin"
341        depfile = "{{output}}.d"
342        command = _rustc_command
343        outputs = [ _output ]
344      }
345
346      tool("rust_rlib") {
347        description = "rustc {{output}}"
348        default_output_dir = "{{target_out_dir}}/lib"
349        depfile = "{{output}}.d"
350        output_prefix = "lib"
351        default_output_extension = ".rlib"
352        command = _rustc_command
353        outputs = [ _output ]
354      }
355
356      tool("rust_staticlib") {
357        description = "rustc {{output}}"
358        default_output_dir = "{{target_out_dir}}/lib"
359        depfile = "{{output}}.d"
360        output_prefix = "lib"
361        default_output_extension = ".a"
362        command = _rustc_command
363        outputs = [ _output ]
364      }
365
366      if (defined(invoker.is_host_toolchain) && invoker.is_host_toolchain) {
367        if (!defined(invoker_toolchain_args.current_os)) {
368          toolchain_os = ""
369        } else {
370          # Determine OS of toolchain, which is the builtin argument "current_os".
371          toolchain_os = invoker_toolchain_args.current_os
372        }
373
374        if (toolchain_os == "mac") {
375          _dylib_extension = ".dylib"
376        } else if (toolchain_os == "win") {
377          _dylib_extension = ".dll"
378        } else {
379          _dylib_extension = ".so"
380        }
381
382        tool("rust_macro") {
383          description = "rustc {{output}}"
384          default_output_dir = "{{target_out_dir}}/lib"
385          depfile = "{{output}}.d"
386          output_prefix = "lib"
387          default_output_extension = _dylib_extension
388          command = _rustc_command
389          outputs = [ _output ]
390        }
391      }
392    }
393  }
394}
395