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