Home | History | Annotate | Download | only in releasetools
      1 #!/usr/bin/env python
      2 #
      3 # Copyright (C) 2009 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 #
     17 
     18 #
     19 # Finds differences between two target files packages
     20 #
     21 
     22 from __future__ import print_function
     23 
     24 import argparse
     25 import contextlib
     26 import os
     27 import re
     28 import subprocess
     29 import sys
     30 import tempfile
     31 
     32 def ignore(name):
     33   """
     34   Files to ignore when diffing
     35 
     36   These are packages that we're already diffing elsewhere,
     37   or files that we expect to be different for every build,
     38   or known problems.
     39   """
     40 
     41   # We're looking at the files that make the images, so no need to search them
     42   if name in ['IMAGES']:
     43     return True
     44   # These are packages of the recovery partition, which we're already diffing
     45   if name in ['SYSTEM/etc/recovery-resource.dat',
     46               'SYSTEM/recovery-from-boot.p']:
     47     return True
     48 
     49   # These files are just the BUILD_NUMBER, and will always be different
     50   if name in ['BOOT/RAMDISK/selinux_version',
     51               'RECOVERY/RAMDISK/selinux_version']:
     52     return True
     53 
     54   return False
     55 
     56 
     57 def rewrite_build_property(original, new):
     58   """
     59   Rewrite property files to remove values known to change for every build
     60   """
     61 
     62   skipped = ['ro.bootimage.build.date=',
     63              'ro.bootimage.build.date.utc=',
     64              'ro.bootimage.build.fingerprint=',
     65              'ro.build.id=',
     66              'ro.build.display.id=',
     67              'ro.build.version.incremental=',
     68              'ro.build.date=',
     69              'ro.build.date.utc=',
     70              'ro.build.host=',
     71              'ro.build.user=',
     72              'ro.build.description=',
     73              'ro.build.fingerprint=',
     74              'ro.expect.recovery_id=',
     75              'ro.vendor.build.date=',
     76              'ro.vendor.build.date.utc=',
     77              'ro.vendor.build.fingerprint=']
     78 
     79   for line in original:
     80     skip = False
     81     for s in skipped:
     82       if line.startswith(s):
     83         skip = True
     84         break
     85     if not skip:
     86       new.write(line)
     87 
     88 
     89 def trim_install_recovery(original, new):
     90   """
     91   Rewrite the install-recovery script to remove the hash of the recovery
     92   partition.
     93   """
     94   for line in original:
     95     new.write(re.sub(r'[0-9a-f]{40}', '0'*40, line))
     96 
     97 def sort_file(original, new):
     98   """
     99   Sort the file. Some OTA metadata files are not in a deterministic order
    100   currently.
    101   """
    102   lines = original.readlines()
    103   lines.sort()
    104   for line in lines:
    105     new.write(line)
    106 
    107 # Map files to the functions that will modify them for diffing
    108 REWRITE_RULES = {
    109     'BOOT/RAMDISK/default.prop': rewrite_build_property,
    110     'RECOVERY/RAMDISK/default.prop': rewrite_build_property,
    111     'SYSTEM/build.prop': rewrite_build_property,
    112     'VENDOR/build.prop': rewrite_build_property,
    113 
    114     'SYSTEM/bin/install-recovery.sh': trim_install_recovery,
    115 
    116     'META/boot_filesystem_config.txt': sort_file,
    117     'META/filesystem_config.txt': sort_file,
    118     'META/recovery_filesystem_config.txt': sort_file,
    119     'META/vendor_filesystem_config.txt': sort_file,
    120 }
    121 
    122 @contextlib.contextmanager
    123 def preprocess(name, filename):
    124   """
    125   Optionally rewrite files before diffing them, to remove known-variable
    126   information.
    127   """
    128   if name in REWRITE_RULES:
    129     with tempfile.NamedTemporaryFile() as newfp:
    130       with open(filename, 'r') as oldfp:
    131         REWRITE_RULES[name](oldfp, newfp)
    132       newfp.flush()
    133       yield newfp.name
    134   else:
    135     yield filename
    136 
    137 def diff(name, file1, file2, out_file):
    138   """
    139   Diff a file pair with diff, running preprocess() on the arguments first.
    140   """
    141   with preprocess(name, file1) as f1:
    142     with preprocess(name, file2) as f2:
    143       proc = subprocess.Popen(['diff', f1, f2], stdout=subprocess.PIPE,
    144                               stderr=subprocess.STDOUT)
    145       (stdout, _) = proc.communicate()
    146       if proc.returncode == 0:
    147         return
    148       stdout = stdout.strip()
    149       if stdout == 'Binary files %s and %s differ' % (f1, f2):
    150         print("%s: Binary files differ" % name, file=out_file)
    151       else:
    152         for line in stdout.strip().split('\n'):
    153           print("%s: %s" % (name, line), file=out_file)
    154 
    155 def recursiveDiff(prefix, dir1, dir2, out_file):
    156   """
    157   Recursively diff two directories, checking metadata then calling diff()
    158   """
    159   list1 = sorted(os.listdir(dir1))
    160   list2 = sorted(os.listdir(dir2))
    161 
    162   for entry in list1:
    163     name = os.path.join(prefix, entry)
    164     name1 = os.path.join(dir1, entry)
    165     name2 = os.path.join(dir2, entry)
    166 
    167     if ignore(name):
    168       continue
    169 
    170     if entry in list2:
    171       if os.path.islink(name1) and os.path.islink(name2):
    172         link1 = os.readlink(name1)
    173         link2 = os.readlink(name2)
    174         if link1 != link2:
    175           print("%s: Symlinks differ: %s vs %s" % (name, link1, link2),
    176                 file=out_file)
    177         continue
    178       elif os.path.islink(name1) or os.path.islink(name2):
    179         print("%s: File types differ, skipping compare" % name, file=out_file)
    180         continue
    181 
    182       stat1 = os.stat(name1)
    183       stat2 = os.stat(name2)
    184       type1 = stat1.st_mode & ~0o777
    185       type2 = stat2.st_mode & ~0o777
    186 
    187       if type1 != type2:
    188         print("%s: File types differ, skipping compare" % name, file=out_file)
    189         continue
    190 
    191       if stat1.st_mode != stat2.st_mode:
    192         print("%s: Modes differ: %o vs %o" %
    193             (name, stat1.st_mode, stat2.st_mode), file=out_file)
    194 
    195       if os.path.isdir(name1):
    196         recursiveDiff(name, name1, name2, out_file)
    197       elif os.path.isfile(name1):
    198         diff(name, name1, name2, out_file)
    199       else:
    200         print("%s: Unknown file type, skipping compare" % name, file=out_file)
    201     else:
    202       print("%s: Only in base package" % name, file=out_file)
    203 
    204   for entry in list2:
    205     name = os.path.join(prefix, entry)
    206     name1 = os.path.join(dir1, entry)
    207     name2 = os.path.join(dir2, entry)
    208 
    209     if ignore(name):
    210       continue
    211 
    212     if entry not in list1:
    213       print("%s: Only in new package" % name, file=out_file)
    214 
    215 def main():
    216   parser = argparse.ArgumentParser()
    217   parser.add_argument('dir1', help='The base target files package (extracted)')
    218   parser.add_argument('dir2', help='The new target files package (extracted)')
    219   parser.add_argument('--output',
    220       help='The output file, otherwise it prints to stdout')
    221   args = parser.parse_args()
    222 
    223   if args.output:
    224     out_file = open(args.output, 'w')
    225   else:
    226     out_file = sys.stdout
    227 
    228   recursiveDiff('', args.dir1, args.dir2, out_file)
    229 
    230   if args.output:
    231     out_file.close()
    232 
    233 if __name__ == '__main__':
    234   main()
    235