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