Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 44c9fc0c authored by Dan Willemsen's avatar Dan Willemsen Committed by android-build-merger
Browse files

Merge "Add tool to diff two target files packages"

am: 5685af31

* commit '5685af31':
  Add tool to diff two target files packages
parents 5188f77e 5685af31
Loading
Loading
Loading
Loading
+224 −0
Original line number Diff line number Diff line
#!/usr/bin/env python
#
# Copyright (C) 2009 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.
#

#
# Finds differences between two target files packages
#

from __future__ import print_function

import argparse
import contextlib
import os
import re
import subprocess
import tempfile

def ignore(name):
  """
  Files to ignore when diffing

  These are packages that we're already diffing elsewhere,
  or files that we expect to be different for every build,
  or known problems.
  """

  # We're looking at the files that make the images, so no need to search them
  if name in ['IMAGES']:
    return True
  # These are packages of the recovery partition, which we're already diffing
  if name in ['SYSTEM/etc/recovery-resource.dat',
              'SYSTEM/recovery-from-boot.p']:
    return True

  # These files are just the BUILD_NUMBER, and will always be different
  if name in ['BOOT/RAMDISK/selinux_version',
              'RECOVERY/RAMDISK/selinux_version']:
    return True

  # b/24201956 .art/.oat/.odex files are different with every build
  if name.endswith('.art') or name.endswith('.oat') or name.endswith('.odex'):
    return True
  # b/25348136 libpac.so changes with every build
  if name in ['SYSTEM/lib/libpac.so',
              'SYSTEM/lib64/libpac.so']:
    return True

  return False


def rewrite_build_property(original, new):
  """
  Rewrite property files to remove values known to change for every build
  """

  skipped = ['ro.bootimage.build.date=',
             'ro.bootimage.build.date.utc=',
             'ro.bootimage.build.fingerprint=',
             'ro.build.id=',
             'ro.build.display.id=',
             'ro.build.version.incremental=',
             'ro.build.date=',
             'ro.build.date.utc=',
             'ro.build.host=',
             'ro.build.description=',
             'ro.build.fingerprint=',
             'ro.expect.recovery_id=',
             'ro.vendor.build.date=',
             'ro.vendor.build.date.utc=',
             'ro.vendor.build.fingerprint=']

  for line in original:
    skip = False
    for s in skipped:
      if line.startswith(s):
        skip = True
        break
    if not skip:
      new.write(line)


def trim_install_recovery(original, new):
  """
  Rewrite the install-recovery script to remove the hash of the recovery partition.
  """
  for line in original:
    new.write(re.sub(r'[0-9a-f]{40}', '0'*40, line))

def sort_file(original, new):
  """
  Sort the file. Some OTA metadata files are not in a deterministic order currently.
  """
  lines = original.readlines()
  lines.sort()
  for line in lines:
    new.write(line)

# Map files to the functions that will modify them for diffing
REWRITE_RULES = {
    'BOOT/RAMDISK/default.prop': rewrite_build_property,
    'RECOVERY/RAMDISK/default.prop': rewrite_build_property,
    'SYSTEM/build.prop': rewrite_build_property,
    'VENDOR/build.prop': rewrite_build_property,

    'SYSTEM/bin/install-recovery.sh': trim_install_recovery,

    'META/boot_filesystem_config.txt': sort_file,
    'META/filesystem_config.txt': sort_file,
    'META/recovery_filesystem_config.txt': sort_file,
    'META/vendor_filesystem_config.txt': sort_file,
}

@contextlib.contextmanager
def preprocess(name, filename):
  """
  Optionally rewrite files before diffing them, to remove known-variable information.
  """
  if name in REWRITE_RULES:
    with tempfile.NamedTemporaryFile() as newfp:
      with open(filename, 'r') as oldfp:
        REWRITE_RULES[name](oldfp, newfp)
      newfp.flush()
      yield newfp.name
  else:
    yield filename

def diff(name, file1, file2):
  """
  Diff a file pair with diff, running preprocess() on the arguments first.
  """
  with preprocess(name, file1) as f1:
    with preprocess(name, file2) as f2:
      proc = subprocess.Popen(['diff', f1, f2], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
      (stdout, ignore) = proc.communicate()
      if proc.returncode == 0:
        return
      stdout = stdout.strip()
      if stdout == 'Binary files %s and %s differ' % (f1, f2):
        print("%s: Binary files differ" % name)
      else:
        for line in stdout.strip().split('\n'):
          print("%s: %s" % (name, line))

def recursiveDiff(prefix, dir1, dir2):
  """
  Recursively diff two directories, checking metadata then calling diff()
  """
  list1 = sorted(os.listdir(dir1))
  list2 = sorted(os.listdir(dir2))

  for entry in list1:
    name = os.path.join(prefix, entry)
    name1 = os.path.join(dir1, entry)
    name2 = os.path.join(dir2, entry)

    if ignore(name):
      continue

    if entry in list2:
      if os.path.islink(name1):
        if os.path.islink(name2):
          link1 = os.readlink(name1)
          link2 = os.readlink(name2)
          if link1 != link2:
            print("%s: Symlinks differ: %s vs %s" % (name, link1, link2))
        else:
          print("%s: File types differ, skipping compare" % name)
        continue

      stat1 = os.stat(name1)
      stat2 = os.stat(name2)
      type1 = stat1.st_mode & ~0777
      type2 = stat2.st_mode & ~0777

      if type1 != type2:
        print("%s: File types differ, skipping compare" % name)
        continue

      if stat1.st_mode != stat2.st_mode:
        print("%s: Modes differ: %o vs %o" % (name, stat1.st_mode, stat2.st_mode))

      if os.path.isdir(name1):
        recursiveDiff(name, name1, name2)
      elif os.path.isfile(name1):
        diff(name, name1, name2)
      else:
        print("%s: Unknown file type, skipping compare" % name)
    else:
      print("%s: Only in base package" % name)

  for entry in list2:
    name = os.path.join(prefix, entry)
    name1 = os.path.join(dir1, entry)
    name2 = os.path.join(dir2, entry)

    if ignore(name):
      continue

    if entry not in list1:
      print("%s: Only in new package" % name)

def main():
  parser = argparse.ArgumentParser()
  parser.add_argument('dir1', help='The base target files package (extracted)')
  parser.add_argument('dir2', help='The new target files package (extracted)')
  args = parser.parse_args()

  recursiveDiff('', args.dir1, args.dir2)

if __name__ == '__main__':
  main()