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# Example 1: Comparing link actions
88# This example gets all of the "CppLink" actions from the adb_test module, and
89# also gets the build actions that are needed to build the same module from
90# through Ninja.
91#
92# After getting the action lists from each build tool, you can inspect the list
93# to find the particular action you're interested in diffing. In this case, there
94# was only 1 CppLink action from Bazel. The corresponding link action from Ninja
95# happened to be the last one (this is pretty typical).
96#
97# Then we set a new variable to keep track of each of these action strings.
98
99bzl_actions = get_bazel_actions(
100    config="linux_x86_64",
101    expr="//packages/modules/adb:adb_test__test_binary_unstripped",
102    mnemonic="CppLink",
103)
104ninja_actions = get_ninja_actions(
105    lunch_target=LUNCH_TARGET,
106    target="out/soong/.intermediates/packages/modules/adb/adb_test/linux_glibc_x86_64/adb_test",
107    mnemonic="clang++",
108)
109bazel_action = bzl_actions[0]["arguments"]
110ninja_action = ninja_actions[-1].split()
111
112# %%
113# Example 2: Comparing compile actions
114# This example is similar and gets all of the "CppCompile" actions from the
115# internal sub-target of adb_test. There is a "CppCompile" action for every
116# .cc file that goes into the target, so we just pick one of these files and
117# get the corresponding compile action from Ninja for this file.
118#
119# Similarly, we select an action from the Bazel list and its corresponding
120# Ninja action.
121
122# bzl_actions = get_bazel_actions(
123#     config="linux_x86_64",
124#     expr="//packages/modules/adb:adb_test__test_binary__internal_root_cpp",
125#     mnemonic="CppCompile",
126# )
127# ninja_actions = get_ninja_actions(
128#     lunch_target=LUNCH_TARGET,
129#     target="out/soong/.intermediates/packages/modules/adb/adb_test/linux_glibc_x86_64/obj/packages/modules/adb/adb_io_test.o",
130#     mnemonic="clang++",
131# )
132# bazel_action = bzl_actions[0]["arguments"]
133# ninja_action = ninja_actions[-1].split()
134
135# %%
136# Example 3: more complex expressions in the Bazel action
137# This example gets all of the "CppCompile" actions from the deps of everything
138# under the //packages/modules/adb package, but it uses the additional_args
139# to exclude "manual" internal targets.
140
141# bzl_actions = get_bazel_actions(
142#     config="linux_x86_64",
143#     expr="deps(//packages/modules/adb/...)",
144#     mnemonic="CppCompile",
145#     additional_args=[
146#         "--build_tag_filters=-manual",
147#     ],
148# )
149
150# %%
151# Once we have the command-line string for each action from Bazel and Ninja,
152# we can use difftool to parse and compare the actions.
153ninja_action = commands.expand_rsp(ninja_action)
154bzl_rich_commands = difftool.rich_command_info(" ".join(bazel_action))
155ninja_rich_commands = difftool.rich_command_info(" ".join(ninja_action))
156
157print("\nBazel args:")
158print(" \\\n\t".join([bzl_rich_commands.tool] + bzl_rich_commands.args))
159print("\nSoong args:")
160print(" \\\n\t".join([ninja_rich_commands.tool] + ninja_rich_commands.args))
161
162bzl_only = bzl_rich_commands.compare(ninja_rich_commands)
163soong_only = ninja_rich_commands.compare(bzl_rich_commands)
164print("\nIn Bazel, not Soong:")
165print(bzl_only)
166print("\nIn Soong, not Bazel:")
167print(soong_only)
168
169# %%
170# Now that we've diffed the action strings, it is sometimes useful to also
171# diff the paths that go into the action. This helps us narrow down diffs
172# in a module that are created in their dependencies. This section attempts
173# to match paths from the Bazel action to corresponding paths in the Ninja
174# action, and the runs difftool on these paths.
175bzl_paths, _ = commands.extract_paths_from_action_args(bazel_action)
176ninja_paths, _ = commands.extract_paths_from_action_args(ninja_action)
177unmatched_paths = []
178for p1, p2 in commands.match_paths(bzl_paths, ninja_paths).items():
179  if p2 is None:
180    unmatched_paths.append(p1)
181    continue
182  diff = difftool.file_differences(
183      pathlib.Path(p1).resolve(),
184      pathlib.Path(p2).resolve(),
185      level=difftool.DiffLevel.FINE,
186  )
187  for row in diff:
188    print(row)
189if unmatched_paths:
190  # Since the test for file paths looks for existing files, this matching won't
191  # work if the Soong artifacts don't exist.
192  print(
193      "Found some Bazel paths that didn't have a good match in Soong "
194      + "intermediates. Did you run `m`?"
195  )
196  print("Unmatched paths:")
197  for i in unmatched_paths:
198    print("\t" + i)
199