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.vendor.build.date=',
     75              'ro.vendor.build.date.utc=',
     76              'ro.vendor.build.fingerprint=']
     77 
     78   for line in original:
     79     skip = False
     80     for s in skipped:
     81       if line.startswith(s):
     82         skip = True
     83         break
     84     if not skip:
     85       new.write(line)
     86 
     87 
     88 def trim_install_recovery(original, new):
     89   """
     90   Rewrite the install-recovery script to remove the hash of the recovery
     91   partition.
     92   """
     93   for line in original:
     94     new.write(re.sub(r'[0-9a-f]{40}', '0'*40, line))
     95 
     96 def sort_file(original, new):
     97   """
     98   Sort the file. Some OTA metadata files are not in a deterministic order
     99   currently.
    100   """
    101   lines = original.readlines()
    102   lines.sort()
    103   for line in lines:
    104     new.write(line)
    105 
    106 # Map files to the functions that will modify them for diffing
    107 REWRITE_RULES = {
    108     'BOOT/RAMDISK/default.prop': rewrite_build_property,
    109     'RECOVERY/RAMDISK/default.prop': rewrite_build_property,
    110     'SYSTEM/build.prop': rewrite_build_property,
    111     'VENDOR/build.prop': rewrite_build_property,
    112 
    113     'SYSTEM/bin/install-recovery.sh': trim_install_recovery,
    114 
    115     'META/boot_filesystem_config.txt': sort_file,
    116     'META/filesystem_config.txt': sort_file,
    117     'META/recovery_filesystem_config.txt': sort_file,
    118     'META/vendor_filesystem_config.txt': sort_file,
    119 }
    120 
    121 @contextlib.contextmanager
    122 def preprocess(name, filename):
    123   """
    124   Optionally rewrite files before diffing them, to remove known-variable
    125   information.
    126   """
    127   if name in REWRITE_RULES:
    128     with tempfile.NamedTemporaryFile() as newfp:
    129       with open(filename, 'r') as oldfp:
    130         REWRITE_RULES[name](oldfp, newfp)
    131       newfp.flush()
    132       yield newfp.name
    133   else:
    134     yield filename
    135 
    136 def diff(name, file1, file2, out_file):
    137   """
    138   Diff a file pair with diff, running preprocess() on the arguments first.
    139   """
    140   with preprocess(name, file1) as f1:
    141     with preprocess(name, file2) as f2:
    142       proc = subprocess.Popen(['diff', f1, f2], stdout=subprocess.PIPE,
    143                               stderr=subprocess.STDOUT)
    144       (stdout, _) = proc.communicate()
    145       if proc.returncode == 0:
    146         return
    147       stdout = stdout.strip()
    148       if stdout == 'Binary files %s and %s differ' % (f1, f2):
    149         print("%s: Binary files differ" % name, file=out_file)
    150       else:
    151         for line in stdout.strip().split('\n'):
    152           print("%s: %s" % (name, line), file=out_file)
    153 
    154 def recursiveDiff(prefix, dir1, dir2, out_file):
    155   """
    156   Recursively diff two directories, checking metadata then calling diff()
    157   """
    158   list1 = sorted(os.listdir(dir1))
    159   list2 = sorted(os.listdir(dir2))
    160 
    161   for entry in list1:
    162     name = os.path.join(prefix, entry)
    163     name1 = os.path.join(dir1, entry)
    164     name2 = os.path.join(dir2, entry)
    165 
    166     if ignore(name):
    167       continue
    168 
    169     if entry in list2:
    170       if os.path.islink(name1) and os.path.islink(name2):
    171         link1 = os.readlink(name1)
    172         link2 = os.readlink(name2)
    173         if link1 != link2:
    174           print("%s: Symlinks differ: %s vs %s" % (name, link1, link2),
    175                 file=out_file)
    176         continue
    177       elif os.path.islink(name1) or os.path.islink(name2):
    178         print("%s: File types differ, skipping compare" % name, file=out_file)
    179         continue
    180 
    181       stat1 = os.stat(name1)
    182       stat2 = os.stat(name2)
    183       type1 = stat1.st_mode & ~0o777
    184       type2 = stat2.st_mode & ~0o777
    185 
    186       if type1 != type2:
    187         print("%s: File types differ, skipping compare" % name, file=out_file)
    188         continue
    189 
    190       if stat1.st_mode != stat2.st_mode:
    191         print("%s: Modes differ: %o vs %o" %
    192             (name, stat1.st_mode, stat2.st_mode), file=out_file)
    193 
    194       if os.path.isdir(name1):
    195         recursiveDiff(name, name1, name2, out_file)
    196       elif os.path.isfile(name1):
    197         diff(name, name1, name2, out_file)
    198       else:
    199         print("%s: Unknown file type, skipping compare" % name, file=out_file)
    200     else:
    201       print("%s: Only in base package" % name, file=out_file)
    202 
    203   for entry in list2:
    204     name = os.path.join(prefix, entry)
    205     name1 = os.path.join(dir1, entry)
    206     name2 = os.path.join(dir2, entry)
    207 
    208     if ignore(name):
    209       continue
    210 
    211     if entry not in list1:
    212       print("%s: Only in new package" % name, file=out_file)
    213 
    214 def main():
    215   parser = argparse.ArgumentParser()
    216   parser.add_argument('dir1', help='The base target files package (extracted)')
    217   parser.add_argument('dir2', help='The new target files package (extracted)')
    218   parser.add_argument('--output',
    219       help='The output file, otherwise it prints to stdout')
    220   args = parser.parse_args()
    221 
    222   if args.output:
    223     out_file = open(args.output, 'w')
    224   else:
    225     out_file = sys.stdout
    226 
    227   recursiveDiff('', args.dir1, args.dir2, out_file)
    228 
    229   if args.output:
    230     out_file.close()
    231 
    232 if __name__ == '__main__':
    233   main()
    234