| #!/usr/bin/env python3 |
| # |
| # Copyright (C) 2022 The Android Open Source Project |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| """Provides useful diff information for build artifacts. |
| |
| This file is intended to be used like a Jupyter notebook. Since there isn't a |
| one-to-one pairing between Soong intermediate artifacts and Bazel intermediate |
| artifacts, I've found it's easiest to automate some of the diffing while |
| leaving room for manual selection of what targets/artifacts to compare. |
| |
| In this file, the runnable sections are separated by the `# %%` identifier, and |
| a compatible editor should be able to run those code blocks independently. I |
| used VSCode during development, but this functionality also exists in other |
| editors via plugins. |
| |
| There are some comments throughout to give an idea of how this notebook can be |
| used. |
| """ |
| |
| # %% |
| import os |
| import pathlib |
| |
| # This script should be run from the $TOP directory |
| ANDROID_CHECKOUT_PATH = pathlib.Path(".").resolve() |
| os.chdir(ANDROID_CHECKOUT_PATH) |
| |
| # %% |
| import subprocess |
| |
| os.chdir(os.path.join(ANDROID_CHECKOUT_PATH, "build/bazel/scripts/difftool")) |
| import difftool |
| import commands |
| import importlib |
| |
| # Python doesn't reload packages that have already been imported unless you |
| # use importlib to explicitly reload them |
| importlib.reload(difftool) |
| importlib.reload(commands) |
| os.chdir(ANDROID_CHECKOUT_PATH) |
| |
| # %% |
| LUNCH_TARGET = "aosp_arm64" |
| TARGET_BUILD_VARIANT = "userdebug" |
| |
| subprocess.run([ |
| "build/soong/soong_ui.bash", |
| "--make-mode", |
| f"TARGET_PRODUCT={LUNCH_TARGET}", |
| f"TARGET_BUILD_VARIANT={TARGET_BUILD_VARIANT}", |
| "--skip-soong-tests", |
| "bp2build", |
| "nothing", |
| ]) |
| |
| |
| # %% |
| def get_bazel_actions( |
| *, expr: str, config: str, mnemonic: str, additional_args: list[str] = [] |
| ): |
| return difftool.collect_commands_bazel( |
| expr, config, mnemonic, *additional_args |
| ) |
| |
| |
| def get_ninja_actions(*, lunch_target: str, target: str, mnemonic: str): |
| ninja_output = difftool.collect_commands_ninja( |
| pathlib.Path(f"out/combined-{lunch_target}.ninja").resolve(), |
| pathlib.Path(target), |
| pathlib.Path("prebuilts/build-tools/linux-x86/bin/ninja").resolve(), |
| ) |
| return [l for l in ninja_output if mnemonic in l] |
| |
| # %% |
| # Example 1: Comparing link actions |
| # This example gets all of the "CppLink" actions from the adb_test module, and |
| # also gets the build actions that are needed to build the same module from |
| # through Ninja. |
| # |
| # After getting the action lists from each build tool, you can inspect the list |
| # to find the particular action you're interested in diffing. In this case, there |
| # was only 1 CppLink action from Bazel. The corresponding link action from Ninja |
| # happened to be the last one (this is pretty typical). |
| # |
| # Then we set a new variable to keep track of each of these action strings. |
| |
| bzl_actions = get_bazel_actions( |
| config="linux_x86_64", |
| expr="//packages/modules/adb:adb_test__test_binary_unstripped", |
| mnemonic="CppLink", |
| ) |
| ninja_actions = get_ninja_actions( |
| lunch_target=LUNCH_TARGET, |
| target="out/soong/.intermediates/packages/modules/adb/adb_test/linux_glibc_x86_64/adb_test", |
| mnemonic="clang++", |
| ) |
| bazel_action = bzl_actions[0]["arguments"] |
| ninja_action = ninja_actions[-1].split() |
| |
| # %% |
| # Example 2: Comparing compile actions |
| # This example is similar and gets all of the "CppCompile" actions from the |
| # internal sub-target of adb_test. There is a "CppCompile" action for every |
| # .cc file that goes into the target, so we just pick one of these files and |
| # get the corresponding compile action from Ninja for this file. |
| # |
| # Similarly, we select an action from the Bazel list and its corresponding |
| # Ninja action. |
| |
| # bzl_actions = get_bazel_actions( |
| # config="linux_x86_64", |
| # expr="//packages/modules/adb:adb_test__test_binary__internal_root_cpp", |
| # mnemonic="CppCompile", |
| # ) |
| # ninja_actions = get_ninja_actions( |
| # lunch_target=LUNCH_TARGET, |
| # target="out/soong/.intermediates/packages/modules/adb/adb_test/linux_glibc_x86_64/obj/packages/modules/adb/adb_io_test.o", |
| # mnemonic="clang++", |
| # ) |
| # bazel_action = bzl_actions[0]["arguments"] |
| # ninja_action = ninja_actions[-1].split() |
| |
| # %% |
| # Example 3: more complex expressions in the Bazel action |
| # This example gets all of the "CppCompile" actions from the deps of everything |
| # under the //packages/modules/adb package, but it uses the additional_args |
| # to exclude "manual" internal targets. |
| |
| # bzl_actions = get_bazel_actions( |
| # config="linux_x86_64", |
| # expr="deps(//packages/modules/adb/...)", |
| # mnemonic="CppCompile", |
| # additional_args=[ |
| # "--build_tag_filters=-manual", |
| # ], |
| # ) |
| |
| # %% |
| # Once we have the command-line string for each action from Bazel and Ninja, |
| # we can use difftool to parse and compare the actions. |
| ninja_action = commands.expand_rsp(ninja_action) |
| bzl_rich_commands = difftool.rich_command_info(" ".join(bazel_action)) |
| ninja_rich_commands = difftool.rich_command_info(" ".join(ninja_action)) |
| |
| print("\nBazel args:") |
| print(" \\\n\t".join([bzl_rich_commands.tool] + bzl_rich_commands.args)) |
| print("\nSoong args:") |
| print(" \\\n\t".join([ninja_rich_commands.tool] + ninja_rich_commands.args)) |
| |
| bzl_only = bzl_rich_commands.compare(ninja_rich_commands) |
| soong_only = ninja_rich_commands.compare(bzl_rich_commands) |
| print("\nIn Bazel, not Soong:") |
| print(bzl_only) |
| print("\nIn Soong, not Bazel:") |
| print(soong_only) |
| |
| # %% |
| # Now that we've diffed the action strings, it is sometimes useful to also |
| # diff the paths that go into the action. This helps us narrow down diffs |
| # in a module that are created in their dependencies. This section attempts |
| # to match paths from the Bazel action to corresponding paths in the Ninja |
| # action, and the runs difftool on these paths. |
| bzl_paths, _ = commands.extract_paths_from_action_args(bazel_action) |
| ninja_paths, _ = commands.extract_paths_from_action_args(ninja_action) |
| unmatched_paths = [] |
| for p1, p2 in commands.match_paths(bzl_paths, ninja_paths).items(): |
| if p2 is None: |
| unmatched_paths.append(p1) |
| continue |
| diff = difftool.file_differences( |
| pathlib.Path(p1).resolve(), |
| pathlib.Path(p2).resolve(), |
| level=difftool.DiffLevel.FINE, |
| ) |
| for row in diff: |
| print(row) |
| if unmatched_paths: |
| # Since the test for file paths looks for existing files, this matching won't |
| # work if the Soong artifacts don't exist. |
| print( |
| "Found some Bazel paths that didn't have a good match in Soong " |
| + "intermediates. Did you run `m`?" |
| ) |
| print("Unmatched paths:") |
| for i in unmatched_paths: |
| print("\t" + i) |