• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2#
3# Copyright (C) 2022 The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9#   http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16"""Provides useful diff information for build artifacts.
17
18This file is intended to be used like a Jupyter notebook. Since there isn't a
19one-to-one pairing between Soong intermediate artifacts and Bazel intermediate
20artifacts, I've found it's easiest to automate some of the diffing while
21leaving room for manual selection of what targets/artifacts to compare.
22
23In this file, the runnable sections are separated by the `# %%` identifier, and
24a compatible editor should be able to run those code blocks independently. I
25used VSCode during development, but this functionality also exists in other
26editors via plugins.
27
28There are some comments throughout to give an idea of how this notebook can be
29used.
30"""
31
32# %%
33import os
34import pathlib
35
36# This script should be run from the $TOP directory
37ANDROID_CHECKOUT_PATH = pathlib.Path(".").resolve()
38os.chdir(ANDROID_CHECKOUT_PATH)
39
40# %%
41import subprocess
42
43os.chdir(os.path.join(ANDROID_CHECKOUT_PATH, "build/bazel/scripts/difftool"))
44import difftool
45import commands
46import importlib
47
48# Python doesn't reload packages that have already been imported unless you
49# use importlib to explicitly reload them
50importlib.reload(difftool)
51importlib.reload(commands)
52os.chdir(ANDROID_CHECKOUT_PATH)
53
54# %%
55LUNCH_TARGET = "aosp_arm64"
56TARGET_BUILD_VARIANT = "userdebug"
57
58subprocess.run([
59    "build/soong/soong_ui.bash",
60    "--make-mode",
61    f"TARGET_PRODUCT={LUNCH_TARGET}",
62    f"TARGET_BUILD_VARIANT={TARGET_BUILD_VARIANT}",
63    "--skip-soong-tests",
64    "bp2build",
65    "nothing",
66])
67
68
69# %%
70def get_bazel_actions(
71    *, expr: str, config: str, mnemonic: str, additional_args: list[str] = []
72):
73  return difftool.collect_commands_bazel(
74      expr, config, mnemonic, *additional_args
75  )
76
77
78def get_ninja_actions(*, lunch_target: str, target: str, mnemonic: str):
79  ninja_output = difftool.collect_commands_ninja(
80      pathlib.Path(f"out/combined-{lunch_target}.ninja").resolve(),
81      pathlib.Path(target),
82      pathlib.Path("prebuilts/build-tools/linux-x86/bin/ninja").resolve(),
83  )
84  return [l for l in ninja_output if mnemonic in l]
85
86
87# %%
88# Example 1: Comparing link actions
89# This example gets all of the "CppLink" actions from the adb_test module, and
90# also gets the build actions that are needed to build the same module from
91# through Ninja.
92#
93# After getting the action lists from each build tool, you can inspect the list
94# to find the particular action you're interested in diffing. In this case, there
95# was only 1 CppLink action from Bazel. The corresponding link action from Ninja
96# happened to be the last one (this is pretty typical).
97#
98# Then we set a new variable to keep track of each of these action strings.
99
100bzl_actions = get_bazel_actions(
101    config="linux_x86_64",
102    expr="//packages/modules/adb:adb_test__test_binary_unstripped",
103    mnemonic="CppLink",
104)
105ninja_actions = get_ninja_actions(
106    lunch_target=LUNCH_TARGET,
107    target="out/soong/.intermediates/packages/modules/adb/adb_test/linux_glibc_x86_64/adb_test",
108    mnemonic="clang++",
109)
110bazel_action = bzl_actions[0]["arguments"]
111ninja_action = ninja_actions[-1].split()
112
113# %%
114# Example 2: Comparing compile actions
115# This example is similar and gets all of the "CppCompile" actions from the
116# internal sub-target of adb_test. There is a "CppCompile" action for every
117# .cc file that goes into the target, so we just pick one of these files and
118# get the corresponding compile action from Ninja for this file.
119#
120# Similarly, we select an action from the Bazel list and its corresponding
121# Ninja action.
122
123# bzl_actions = get_bazel_actions(
124#     config="linux_x86_64",
125#     expr="//packages/modules/adb:adb_test__test_binary__internal_root_cpp",
126#     mnemonic="CppCompile",
127# )
128# ninja_actions = get_ninja_actions(
129#     lunch_target=LUNCH_TARGET,
130#     target="out/soong/.intermediates/packages/modules/adb/adb_test/linux_glibc_x86_64/obj/packages/modules/adb/adb_io_test.o",
131#     mnemonic="clang++",
132# )
133# bazel_action = bzl_actions[0]["arguments"]
134# ninja_action = ninja_actions[-1].split()
135
136# %%
137# Example 3: more complex expressions in the Bazel action
138# This example gets all of the "CppCompile" actions from the deps of everything
139# under the //packages/modules/adb package, but it uses the additional_args
140# to exclude "manual" internal targets.
141
142# bzl_actions = get_bazel_actions(
143#     config="linux_x86_64",
144#     expr="deps(//packages/modules/adb/...)",
145#     mnemonic="CppCompile",
146#     additional_args=[
147#         "--build_tag_filters=-manual",
148#     ],
149# )
150
151# %%
152# Once we have the command-line string for each action from Bazel and Ninja,
153# we can use difftool to parse and compare the actions.
154ninja_action = commands.expand_rsp(ninja_action)
155bzl_rich_commands = difftool.rich_command_info(" ".join(bazel_action))
156ninja_rich_commands = difftool.rich_command_info(" ".join(ninja_action))
157
158print("Bazel args:")
159print(" \\\n\t".join([bzl_rich_commands.tool] + bzl_rich_commands.args))
160print("Soong args:")
161print(" \\\n\t".join([ninja_rich_commands.tool] + ninja_rich_commands.args))
162
163bzl_only = bzl_rich_commands.compare(ninja_rich_commands)
164soong_only = ninja_rich_commands.compare(bzl_rich_commands)
165print("In Bazel, not Soong:")
166print(bzl_only)
167print("In Soong, not Bazel:")
168print(soong_only)
169
170# %%
171# Now that we've diffed the action strings, it is sometimes useful to also
172# diff the paths that go into the action. This helps us narrow down diffs
173# in a module that are created in their dependencies. This section attempts
174# to match paths from the Bazel action to corresponding paths in the Ninja
175# action, and the runs difftool on these paths.
176bzl_paths, _ = commands.extract_paths_from_action_args(bazel_action)
177ninja_paths, _ = commands.extract_paths_from_action_args(ninja_action)
178unmatched_paths = []
179for p1, p2 in commands.match_paths(bzl_paths, ninja_paths).items():
180  if p2 is None:
181    unmatched_paths.append(p1)
182    continue
183  diff = difftool.file_differences(
184      pathlib.Path(p1).resolve(),
185      pathlib.Path(p2).resolve(),
186      level=difftool.DiffLevel.FINE,
187  )
188  for row in diff:
189    print(row)
190if unmatched_paths:
191  # Since the test for file paths looks for existing files, this matching won't
192  # work if the Soong artifacts don't exist.
193  print(
194      "Found some Bazel paths that didn't have a good match in Soong "
195      + "intermediates. Did you run `m`?"
196  )
197  print("Unmatched paths:")
198  for i in unmatched_paths:
199    print("\t" + i)
200