xref: /aosp_15_r20/external/pigweed/pw_bloat/bloat.gni (revision 61c4878ac05f98d0ceed94b57d316916de578985)
1# Copyright 2023 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_build/evaluate_path_expressions.gni")
18import("$dir_pw_build/python_action.gni")
19
20declare_args() {
21  # Path to the Bloaty configuration file that defines the memory layout and
22  # capacities for the target binaries.
23  pw_bloat_BLOATY_CONFIG = ""
24
25  # List of toolchains to use in pw_toolchain_size_diff templates.
26  #
27  # Each entry is a scope containing the following variables:
28  #
29  #   name: Human-readable toolchain name.
30  #   target: GN target that defines the toolchain.
31  #   linker_script: Optional path to a linker script file to build for the
32  #     toolchain's target.
33  #   bloaty_config: Optional Bloaty confirugation file defining the memory
34  #     layout of the binaries as specified in the linker script.
35  #
36  # If this list is empty, pw_toolchain_size_diff targets become no-ops.
37  pw_bloat_TOOLCHAINS = []
38
39  # Controls whether to display size reports in the build output.
40  pw_bloat_SHOW_SIZE_REPORTS = false
41}
42
43# Creates a size report for a single binary.
44#
45# Args:
46#   target: Build target for executable. Required.
47#   data_sources: List of datasources from bloaty config file
48#     or built-in datasources. Order of sources determines hierarchical
49#     output. Optional.
50#     github.com/google/bloaty/blob/a1bbc93f5f6f969242046dffd9deb379f6735020/doc/using.md
51#   source_filter: Regex to filter data source names in Bloaty. Optional.
52#   json_key_prefix: Prefix for the key names in json size report. Defaults to
53#     target name. Optional.
54#   full_json_summary: If true, json report includes size breakdown per source
55#     hierarchy. Otherwise, defaults to only include the top-level data source
56#     type in size report. Optional.
57#   ignore_unused_labels: If true, json report won't include labels that have
58#     size equal to zero. Optional.
59#
60# Example:
61#   pw_size_report("foo_bloat") {
62#      target = ":foo_static"
63#      datasources = "symbols,segment_names"
64#      source_filter = "foo"
65#      json_key_prefix = "foo"
66#      full_json_summary = true
67#      ignore_unused_labels = true
68#   }
69#
70template("pw_size_report") {
71  if (pw_bloat_BLOATY_CONFIG != "") {
72    assert(defined(invoker.target),
73           "Size report must defined a 'target' variable")
74    _all_target_dependencies = [ invoker.target ]
75    _binary_args = []
76
77    if (defined(invoker.source_filter)) {
78      curr_source_filter = invoker.source_filter
79    } else {
80      curr_source_filter = ""
81    }
82
83    if (defined(invoker.data_sources)) {
84      curr_data_sources = string_split(invoker.data_sources, ",")
85    } else {
86      curr_data_sources = ""
87    }
88    _binary_args = [
89      {
90        bloaty_config = rebase_path(pw_bloat_BLOATY_CONFIG, root_build_dir)
91        out_dir = rebase_path(target_gen_dir, root_build_dir)
92        target = "<TARGET_FILE(${invoker.target})>"
93        source_filter = curr_source_filter
94        data_sources = curr_data_sources
95      },
96    ]
97
98    _file_name = "${target_name}_single_binary.json"
99
100    _args_src = "$target_gen_dir/${_file_name}.in"
101    _args_path = "$target_gen_dir/${_file_name}"
102
103    write_file(_args_src,
104               {
105                 binaries = _binary_args
106                 target_name = target_name
107                 out_dir = rebase_path(target_gen_dir, root_build_dir)
108                 root = rebase_path("//", root_build_dir)
109                 toolchain = current_toolchain
110                 default_toolchain = default_toolchain
111                 cwd = rebase_path(".", root_build_dir)
112               },
113               "json")
114
115    pw_evaluate_path_expressions("${target_name}.evaluate") {
116      files = [
117        {
118          source = _args_src
119          dest = _args_path
120        },
121      ]
122    }
123
124    _bloat_script_args = [
125      "--gn-arg-path",
126      rebase_path(_args_path, root_build_dir),
127      "--single-report",
128    ]
129
130    if (defined(invoker.json_key_prefix)) {
131      _bloat_script_args += [
132        "--json-key-prefix",
133        invoker.json_key_prefix,
134      ]
135    }
136
137    if (defined(invoker.full_json_summary)) {
138      if (invoker.full_json_summary) {
139        _bloat_script_args += [ "--full-json-summary" ]
140      }
141    }
142
143    if (defined(invoker.ignore_unused_labels)) {
144      if (invoker.ignore_unused_labels) {
145        _bloat_script_args += [ "--ignore-unused-labels" ]
146      }
147    }
148
149    _doc_rst_output = "$target_gen_dir/${target_name}"
150    _binary_sizes_output = "$target_gen_dir/${target_name}.binary_sizes.json"
151
152    if (host_os == "win") {
153      # Bloaty is not yet packaged for Windows systems; display a message
154      # indicating this.
155      not_needed("*")
156      not_needed(invoker, "*")
157
158      pw_python_action(target_name) {
159        metadata = {
160          pw_doc_sources = rebase_path([ _doc_rst_output ], root_build_dir)
161        }
162        script = "$dir_pw_bloat/py/pw_bloat/no_bloaty.py"
163        python_deps = [ "$dir_pw_bloat/py" ]
164        args = [ rebase_path(_doc_rst_output, root_build_dir) ]
165        outputs = [ _doc_rst_output ]
166      }
167
168      group(target_name + "_UNUSED_DEPS") {
169        deps = _all_target_dependencies
170      }
171    } else {
172      # Create an action which runs the size report script on the provided
173      # targets.
174      pw_python_action(target_name) {
175        metadata = {
176          pw_doc_sources = rebase_path([ _doc_rst_output ], root_build_dir)
177        }
178        script = "$dir_pw_bloat/py/pw_bloat/bloat.py"
179        python_deps = [ "$dir_pw_bloat/py" ]
180        inputs = [
181          pw_bloat_BLOATY_CONFIG,
182          _args_path,
183        ]
184        outputs = [
185          "${_doc_rst_output}.txt",
186          _binary_sizes_output,
187          _doc_rst_output,
188        ]
189        deps = _all_target_dependencies + [ ":${target_name}.evaluate" ]
190        args = _bloat_script_args
191
192        # Print size reports to stdout when they are generated, if requested.
193        capture_output = !pw_bloat_SHOW_SIZE_REPORTS
194      }
195    }
196  } else {
197    not_needed(invoker, "*")
198    group(target_name) {
199    }
200  }
201}
202
203# Aggregates JSON size report data from several pw_size_report targets into a
204# single output file.
205#
206# Args:
207#   deps: List of pw_size_report targets whose data to collect.
208#   output: Path to the output JSON file.
209#
210# Example:
211#   pw_size_report_aggregation("image_sizes") {
212#      deps = [
213#        ":app_image_size_report",
214#        ":bootloader_image_size_report",
215#      ]
216#      output = "$root_gen_dir/artifacts/image_sizes.json"
217#   }
218#
219template("pw_size_report_aggregation") {
220  assert(defined(invoker.deps) && invoker.deps != [],
221         "pw_size_report_aggregation requires size report dependencies")
222  assert(defined(invoker.output),
223         "pw_size_report_aggregation requires an output file path")
224
225  _input_json_files = []
226
227  foreach(_dep, invoker.deps) {
228    _gen_dir = get_label_info(_dep, "target_gen_dir")
229    _dep_name = get_label_info(_dep, "name")
230    _input_json_files +=
231        [ rebase_path("$_gen_dir/${_dep_name}.binary_sizes.json",
232                      root_build_dir) ]
233  }
234
235  pw_python_action(target_name) {
236    script = "$dir_pw_bloat/py/pw_bloat/binary_size_aggregator.py"
237    python_deps = [ "$dir_pw_bloat/py" ]
238    args = [
239             "--output",
240             rebase_path(invoker.output, root_build_dir),
241           ] + _input_json_files
242    outputs = [ invoker.output ]
243    deps = invoker.deps
244    forward_variables_from(invoker, [ "visibility" ])
245  }
246}
247
248# Creates a target which runs a size report diff on a set of executables.
249#
250# Args:
251#   base: The default base executable target to run the diff against. May be
252#     omitted if all binaries provide their own base.
253#   source_filter: Optional global regex to filter data source names in Bloaty.
254#   data_sources: List of datasources from bloaty config file
255#     or built-in datasources. Order of sources determines hierarchical
256#     output. Optional.
257#     github.com/google/bloaty/blob/a1bbc93f5f6f969242046dffd9deb379f6735020/doc/using.md
258#   binaries: List of executables to compare in the diff.
259#     Each binary in the list is a scope containing up to three variables:
260#       label: Descriptive name for the executable. Required.
261#       target: Build target for the executable. Required.
262#       base: Optional base diff target. Overrides global base argument.
263#       source_filter: Optional regex to filter data source names.
264#         Overrides global source_filter argument.
265#       data_sources: Optional List of datasources from bloaty config file
266#         Overrides global data_sources argument.
267#
268#
269# Example:
270#   pw_size_diff("foo_bloat") {
271#     base = ":foo_base"
272#     data_sources = "segment,symbols"
273#     binaries = [
274#       {
275#         target = ":foo_static"
276#         label = "Static"
277#       },
278#       {
279#         target = ":foo_dynamic"
280#         label = "Dynamic"
281#         data_sources = "segment_names"
282#       },
283#     ]
284#   }
285#
286template("pw_size_diff") {
287  if (pw_bloat_BLOATY_CONFIG != "") {
288    if (defined(invoker.base)) {
289      _global_base = invoker.base
290      _all_target_dependencies = [ _global_base ]
291    } else {
292      _all_target_dependencies = []
293    }
294
295    if (defined(invoker.source_filter)) {
296      _global_source_filter = invoker.source_filter
297    }
298
299    if (defined(invoker.data_sources)) {
300      _global_data_sources = string_split(invoker.data_sources, ",")
301    }
302
303    # TODO(brandonvu): Remove once all downstream projects are updated
304    if (defined(invoker.title)) {
305      not_needed(invoker, [ "title" ])
306    }
307
308    # This template creates an action which invokes a Python script to run a
309    # size report on each of the provided targets. Each of the targets is listed
310    # as a dependency of the action so that the report gets updated when
311    # anything is changed. Most of the code below builds the command-line
312    # arguments to pass each of the targets into the script.
313
314    # Process each of the binaries, creating an object and storing all the
315    # needed variables into a json. Json is parsed in bloat.py
316    _binaries_args = []
317    _bloaty_configs = []
318
319    foreach(binary, invoker.binaries) {
320      assert(defined(binary.label) && defined(binary.target),
321             "Size report binaries must define 'label' and 'target' variables")
322      _all_target_dependencies += [ binary.target ]
323
324      # If the binary defines its own base, use that instead of the global base.
325      if (defined(binary.base)) {
326        _binary_base = binary.base
327        _all_target_dependencies += [ _binary_base ]
328      } else if (defined(_global_base)) {
329        _binary_base = _global_base
330      } else {
331        assert(false, "pw_size_diff requires a 'base' file")
332      }
333
334      if (defined(binary.source_filter)) {
335        _binary_source_filter = binary.source_filter
336      } else if (defined(_global_source_filter)) {
337        _binary_source_filter = _global_source_filter
338      } else {
339        _binary_source_filter = ""
340      }
341
342      _binary_data_sources = []
343      if (defined(binary.data_sources)) {
344        _binary_data_sources = string_split(binary.data_sources, ",")
345      } else if (defined(_global_data_sources)) {
346        _binary_data_sources = _global_data_sources
347      } else {
348        _binary_data_sources = ""
349      }
350
351      # Allow each binary to override the global bloaty config.
352      if (defined(binary.bloaty_config)) {
353        _binary_bloaty_config = binary.bloaty_config
354        _bloaty_configs += [ binary.bloaty_config ]
355      } else {
356        _binary_bloaty_config = pw_bloat_BLOATY_CONFIG
357        _bloaty_configs += [ pw_bloat_BLOATY_CONFIG ]
358      }
359
360      _binaries_args += [
361        {
362          bloaty_config = rebase_path(_binary_bloaty_config, root_build_dir)
363          target = "<TARGET_FILE(${binary.target})>"
364          base = "<TARGET_FILE($_binary_base)>"
365          source_filter = _binary_source_filter
366          label = binary.label
367          data_sources = _binary_data_sources
368        },
369      ]
370    }
371
372    _file_name = "${target_name}_binaries.json"
373    _diff_source = "$target_gen_dir/${_file_name}.in"
374    _diff_path = "$target_gen_dir/${_file_name}"
375    write_file(_diff_source,
376               {
377                 binaries = _binaries_args
378                 target_name = target_name
379                 out_dir = rebase_path(target_gen_dir, root_build_dir)
380                 root = rebase_path("//", root_build_dir)
381                 toolchain = current_toolchain
382                 default_toolchain = default_toolchain
383                 cwd = rebase_path(".", root_build_dir)
384               },
385               "json")
386
387    pw_evaluate_path_expressions("${target_name}.evaluate") {
388      files = [
389        {
390          source = _diff_source
391          dest = _diff_path
392        },
393      ]
394    }
395
396    _bloat_script_args = [
397      "--gn-arg-path",
398      rebase_path(_diff_path, root_build_dir),
399    ]
400
401    # TODO(brandonvu): Remove once all downstream projects are updated
402    if (defined(invoker.full_report)) {
403      not_needed(invoker, [ "full_report" ])
404    }
405
406    _doc_rst_output = "$target_gen_dir/${target_name}"
407
408    if (host_os == "win") {
409      # Bloaty is not yet packaged for Windows systems; display a message
410      # indicating this.
411      not_needed("*")
412      not_needed(invoker, "*")
413
414      pw_python_action(target_name) {
415        metadata = {
416          pw_doc_sources = rebase_path([ _doc_rst_output ], root_build_dir)
417        }
418        script = "$dir_pw_bloat/py/pw_bloat/no_bloaty.py"
419        python_deps = [ "$dir_pw_bloat/py" ]
420        args = [ rebase_path(_doc_rst_output, root_build_dir) ]
421        outputs = [ _doc_rst_output ]
422      }
423
424      group(target_name + "_UNUSED_DEPS") {
425        deps = _all_target_dependencies
426      }
427    } else {
428      # Create an action which runs the size report script on the provided
429      # targets.
430      pw_python_action(target_name) {
431        metadata = {
432          pw_doc_sources = rebase_path([ _doc_rst_output ], root_build_dir)
433        }
434        script = "$dir_pw_bloat/py/pw_bloat/bloat.py"
435        python_deps = [ "$dir_pw_bloat/py" ]
436        inputs = _bloaty_configs + [ _diff_path ]
437        outputs = [
438          "${_doc_rst_output}.txt",
439          _doc_rst_output,
440        ]
441        deps = _all_target_dependencies + [ ":${target_name}.evaluate" ]
442        args = _bloat_script_args
443
444        # Print size reports to stdout when they are generated, if requested.
445        capture_output = !pw_bloat_SHOW_SIZE_REPORTS
446      }
447    }
448  } else {
449    not_needed(invoker, "*")
450    group(target_name) {
451    }
452  }
453}
454
455# Creates a report card comparing the sizes of the same binary compiled with
456# different toolchains. The toolchains to use are listed in the build variable
457# pw_bloat_TOOLCHAINS.
458#
459# Args:
460#   base_executable: Scope containing a list of variables defining an executable
461#     target for the size report base.
462#   diff_executable: Scope containing a list of variables defining an executable
463#     target for the size report comparison.
464#
465# Outputs:
466#   $target_gen_dir/$target_name.txt
467#   $target_gen_dir/$target_name.rst
468#
469# Example:
470#
471#   pw_toolchain_size_diff("my_size_report") {
472#     base_executable = {
473#       sources = [ "base.cc" ]
474#     }
475#
476#     diff_executable = {
477#       sources = [ "base_with_libfoo.cc" ]
478#       deps = [ ":libfoo" ]
479#     }
480#   }
481#
482template("pw_toolchain_size_diff") {
483  assert(defined(invoker.base_executable),
484         "pw_toolchain_size_diff requires a base_executable")
485  assert(defined(invoker.diff_executable),
486         "pw_toolchain_size_diff requires a diff_executable")
487
488  _size_report_binaries = []
489
490  # Multiple build targets are created for each toolchain, which all need unique
491  # target names, so throw a counter in there.
492  i = 0
493
494  # Create a base and diff executable for each toolchain, adding the toolchain's
495  # linker script to the link flags for the executable, and add them all to a
496  # list of binaries for the pw_size_diff template.
497  foreach(_toolchain, pw_bloat_TOOLCHAINS) {
498    _prefix = "_${target_name}_${i}_pw_size"
499
500    # Create a config which adds the toolchain's linker script as a linker flag
501    # if the toolchain provides one.
502    _linker_script_target_name = "${_prefix}_linker_script"
503    config(_linker_script_target_name) {
504      if (defined(_toolchain.linker_script)) {
505        ldflags =
506            [ "-T" + rebase_path(_toolchain.linker_script, root_build_dir) ]
507        inputs = [ _toolchain.linker_script ]
508      } else {
509        ldflags = []
510      }
511    }
512
513    # Create a group which forces the linker script config its dependents.
514    _linker_group_target_name = "${_prefix}_linker_group"
515    group(_linker_group_target_name) {
516      public_configs = [ ":$_linker_script_target_name" ]
517    }
518
519    # Define the size report base executable with the toolchain's linker script.
520    _base_target_name = "${_prefix}_base"
521    executable(_base_target_name) {
522      forward_variables_from(invoker.base_executable, "*")
523      if (!defined(deps)) {
524        deps = []
525      }
526      deps += [ ":$_linker_group_target_name" ]
527    }
528
529    # Define the size report diff executable with the toolchain's linker script.
530    _diff_target_name = "${_prefix}_diff"
531    executable(_diff_target_name) {
532      forward_variables_from(invoker.diff_executable, "*")
533      if (!defined(deps)) {
534        deps = []
535      }
536      deps += [ ":$_linker_group_target_name" ]
537    }
538
539    # Force compilation with the toolchain.
540    _base_label = get_label_info(":$_base_target_name", "label_no_toolchain")
541    _base_with_toolchain = "$_base_label(${_toolchain.target})"
542    _diff_label = get_label_info(":$_diff_target_name", "label_no_toolchain")
543    _diff_with_toolchain = "$_diff_label(${_toolchain.target})"
544
545    # Append a pw_size_diff binary scope to the list comparing the toolchain's
546    # diff and base executables.
547    _size_report_binaries += [
548      {
549        base = _base_with_toolchain
550        target = _diff_with_toolchain
551        label = _toolchain.name
552
553        if (defined(_toolchain.bloaty_config)) {
554          bloaty_config = _toolchain.bloaty_config
555        }
556      },
557    ]
558
559    i += 1
560  }
561
562  # TODO(frolv): Have a way of indicating that a toolchain should build docs.
563  if (current_toolchain == default_toolchain && _size_report_binaries != []) {
564    # Create the size report which runs on the binaries.
565    pw_size_diff(target_name) {
566      forward_variables_from(invoker, [ "title" ])
567      binaries = _size_report_binaries
568    }
569  } else {
570    # If no toolchains are listed in pw_bloat_TOOLCHAINS, prevent GN from
571    # complaining about unused variables and run a script that outputs a ReST
572    # warning to the size report file.
573    not_needed("*")
574    not_needed(invoker, "*")
575
576    _doc_rst_output = "$target_gen_dir/$target_name"
577    pw_python_action(target_name) {
578      metadata = {
579        pw_doc_sources = rebase_path([ _doc_rst_output ], root_build_dir)
580      }
581      script = "$dir_pw_bloat/py/pw_bloat/no_toolchains.py"
582      python_deps = [ "$dir_pw_bloat/py" ]
583      args = [ rebase_path(_doc_rst_output, root_build_dir) ]
584      outputs = [ _doc_rst_output ]
585    }
586  }
587}
588
589# A base_executable for the pw_toolchain_size_diff template which contains a
590# main() function that loads the bloat_this_binary library and does nothing
591# else.
592pw_bloat_empty_base = {
593  deps = [
594    "$dir_pw_bloat:base_main",
595    "$dir_pw_bloat:bloat_this_binary",
596  ]
597}
598