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