blob: 82e4e17d165899665bb53734780897839ffb6236 [file] [log] [blame]
Caroline Ticef6ef4392017-04-06 17:16:05 -07001#!/usr/bin/env python2
Han Shen3dc2a882014-01-07 16:42:01 -08002"""Diff 2 chromiumos images by comparing each elf file.
3
Caroline Tice88272d42016-01-13 09:48:29 -08004 The script diffs every *ELF* files by dissembling every *executable*
5 section, which means it is not a FULL elf differ.
Han Shen3dc2a882014-01-07 16:42:01 -08006
7 A simple usage example -
8 chromiumos_image_diff.py --image1 image-path-1 --image2 image-path-2
9
10 Note that image path should be inside the chroot, if not (ie, image is
11 downloaded from web), please specify a chromiumos checkout via
12 "--chromeos_root".
13
14 And this script should be executed outside chroot.
15"""
16
Caroline Tice88272d42016-01-13 09:48:29 -080017from __future__ import print_function
18
Han Shen3dc2a882014-01-07 16:42:01 -080019__author__ = 'shenhan@google.com (Han Shen)'
20
21import argparse
22import os
23import re
24import sys
25import tempfile
26
27import image_chromeos
Caroline Tice88272d42016-01-13 09:48:29 -080028from cros_utils import command_executer
29from cros_utils import logger
30from cros_utils import misc
Han Shen3dc2a882014-01-07 16:42:01 -080031
32
33class CrosImage(object):
34 """A cros image object."""
35
36 def __init__(self, image, chromeos_root, no_unmount):
37 self.image = image
38 self.chromeos_root = chromeos_root
39 self.mounted = False
40 self._ce = command_executer.GetCommandExecuter()
41 self.logger = logger.GetLogger()
42 self.elf_files = []
43 self.no_unmount = no_unmount
Caroline Tice88272d42016-01-13 09:48:29 -080044 self.unmount_script = ''
45 self.stateful = ''
46 self.rootfs = ''
Han Shen3dc2a882014-01-07 16:42:01 -080047
48 def MountImage(self, mount_basename):
49 """Mount/unpack the image."""
50
51 if mount_basename:
52 self.rootfs = '/tmp/{0}.rootfs'.format(mount_basename)
53 self.stateful = '/tmp/{0}.stateful'.format(mount_basename)
54 self.unmount_script = '/tmp/{0}.unmount.sh'.format(mount_basename)
55 else:
Caroline Ticef6ef4392017-04-06 17:16:05 -070056 self.rootfs = tempfile.mkdtemp(
57 suffix='.rootfs', prefix='chromiumos_image_diff')
Han Shen3dc2a882014-01-07 16:42:01 -080058 ## rootfs is like /tmp/tmpxyz012.rootfs.
59 match = re.match(r'^(.*)\.rootfs$', self.rootfs)
60 basename = match.group(1)
61 self.stateful = basename + '.stateful'
62 os.mkdir(self.stateful)
63 self.unmount_script = '{0}.unmount.sh'.format(basename)
64
65 self.logger.LogOutput('Mounting "{0}" onto "{1}" and "{2}"'.format(
66 self.image, self.rootfs, self.stateful))
67 ## First of all creating an unmount image
68 self.CreateUnmountScript()
69 command = image_chromeos.GetImageMountCommand(
70 self.chromeos_root, self.image, self.rootfs, self.stateful)
Luis Lozano036c9232015-12-10 10:47:01 -080071 rv = self._ce.RunCommand(command, print_to_console=True)
Han Shen3dc2a882014-01-07 16:42:01 -080072 self.mounted = (rv == 0)
73 if not self.mounted:
74 self.logger.LogError('Failed to mount "{0}" onto "{1}" and "{2}".'.format(
75 self.image, self.rootfs, self.stateful))
76 return self.mounted
77
78 def CreateUnmountScript(self):
79 command = ('sudo umount {r}/usr/local {r}/usr/share/oem '
80 '{r}/var {r}/mnt/stateful_partition {r}; sudo umount {s} ; '
Caroline Ticef6ef4392017-04-06 17:16:05 -070081 'rmdir {r} ; rmdir {s}\n').format(
82 r=self.rootfs, s=self.stateful)
Han Shen3dc2a882014-01-07 16:42:01 -080083 f = open(self.unmount_script, 'w')
84 f.write(command)
85 f.close()
Caroline Ticef6ef4392017-04-06 17:16:05 -070086 self._ce.RunCommand(
87 'chmod +x {}'.format(self.unmount_script), print_to_console=False)
88 self.logger.LogOutput(
89 'Created an unmount script - "{0}"'.format(self.unmount_script))
Han Shen3dc2a882014-01-07 16:42:01 -080090
91 def UnmountImage(self):
92 """Unmount the image and delete mount point."""
93
94 self.logger.LogOutput('Unmounting image "{0}" from "{1}" and "{2}"'.format(
95 self.image, self.rootfs, self.stateful))
96 if self.mounted:
97 command = 'bash "{0}"'.format(self.unmount_script)
98 if self.no_unmount:
99 self.logger.LogOutput(('Please unmount manually - \n'
100 '\t bash "{0}"'.format(self.unmount_script)))
101 else:
Luis Lozano036c9232015-12-10 10:47:01 -0800102 if self._ce.RunCommand(command, print_to_console=True) == 0:
Han Shen3dc2a882014-01-07 16:42:01 -0800103 self._ce.RunCommand('rm {0}'.format(self.unmount_script))
104 self.mounted = False
105 self.rootfs = None
106 self.stateful = None
107 self.unmount_script = None
108
Caroline Tice88272d42016-01-13 09:48:29 -0800109 return not self.mounted
Han Shen3dc2a882014-01-07 16:42:01 -0800110
111 def FindElfFiles(self):
112 """Find all elf files for the image.
113
114 Returns:
115 Always true
116 """
117
Caroline Ticef6ef4392017-04-06 17:16:05 -0700118 self.logger.LogOutput(
119 'Finding all elf files in "{0}" ...'.format(self.rootfs))
Han Shen3dc2a882014-01-07 16:42:01 -0800120 # Note '\;' must be prefixed by 'r'.
121 command = ('find "{0}" -type f -exec '
Caroline Ticef6ef4392017-04-06 17:16:05 -0700122 'bash -c \'file -b "{{}}" | grep -q "ELF"\''
123 r' \; '
Han Shen3dc2a882014-01-07 16:42:01 -0800124 r'-exec echo "{{}}" \;').format(self.rootfs)
125 self.logger.LogCmd(command)
Luis Lozano036c9232015-12-10 10:47:01 -0800126 _, out, _ = self._ce.RunCommandWOutput(command, print_to_console=False)
Han Shen3dc2a882014-01-07 16:42:01 -0800127 self.elf_files = out.splitlines()
128 self.logger.LogOutput(
129 'Total {0} elf files found.'.format(len(self.elf_files)))
130 return True
131
132
133class ImageComparator(object):
134 """A class that wraps comparsion actions."""
135
136 def __init__(self, images, diff_file):
137 self.images = images
138 self.logger = logger.GetLogger()
139 self.diff_file = diff_file
140 self.tempf1 = None
141 self.tempf2 = None
142
143 def Cleanup(self):
144 if self.tempf1 and self.tempf2:
145 command_executer.GetCommandExecuter().RunCommand(
146 'rm {0} {1}'.format(self.tempf1, self.tempf2))
Caroline Ticef6ef4392017-04-06 17:16:05 -0700147 logger.GetLogger(
148 'Removed "{0}" and "{1}".'.format(self.tempf1, self.tempf2))
Han Shen3dc2a882014-01-07 16:42:01 -0800149
150 def CheckElfFileSetEquality(self):
151 """Checking whether images have exactly number of elf files."""
152
153 self.logger.LogOutput('Checking elf file equality ...')
154 i1 = self.images[0]
155 i2 = self.images[1]
156 t1 = i1.rootfs + '/'
157 elfset1 = set([e.replace(t1, '') for e in i1.elf_files])
158 t2 = i2.rootfs + '/'
159 elfset2 = set([e.replace(t2, '') for e in i2.elf_files])
160 dif1 = elfset1.difference(elfset2)
161 msg = None
162 if dif1:
163 msg = 'The following files are not in "{image}" - "{rootfs}":\n'.format(
164 image=i2.image, rootfs=i2.rootfs)
165 for d in dif1:
166 msg += '\t' + d + '\n'
167 dif2 = elfset2.difference(elfset1)
168 if dif2:
169 msg = 'The following files are not in "{image}" - "{rootfs}":\n'.format(
170 image=i1.image, rootfs=i1.rootfs)
171 for d in dif2:
172 msg += '\t' + d + '\n'
173 if msg:
174 self.logger.LogError(msg)
175 return False
176 return True
177
178 def CompareImages(self):
179 """Do the comparsion work."""
180
181 if not self.CheckElfFileSetEquality():
182 return False
183
184 mismatch_list = []
185 match_count = 0
186 i1 = self.images[0]
187 i2 = self.images[1]
Caroline Ticef6ef4392017-04-06 17:16:05 -0700188 self.logger.LogOutput(
189 'Start comparing {0} elf file by file ...'.format(len(i1.elf_files)))
Han Shen3dc2a882014-01-07 16:42:01 -0800190 ## Note - i1.elf_files and i2.elf_files have exactly the same entries here.
191
192 ## Create 2 temp files to be used for all disassembed files.
193 handle, self.tempf1 = tempfile.mkstemp()
194 os.close(handle) # We do not need the handle
195 handle, self.tempf2 = tempfile.mkstemp()
196 os.close(handle)
197
198 cmde = command_executer.GetCommandExecuter()
199 for elf1 in i1.elf_files:
200 tmp_rootfs = i1.rootfs + '/'
201 f1 = elf1.replace(tmp_rootfs, '')
202 full_path1 = elf1
203 full_path2 = elf1.replace(i1.rootfs, i2.rootfs)
204
205 if full_path1 == full_path2:
206 self.logger.LogError(
207 'Error: We\'re comparing the SAME file - {0}'.format(f1))
208 continue
209
Caroline Ticef6ef4392017-04-06 17:16:05 -0700210 command = (
211 'objdump -d "{f1}" > {tempf1} ; '
212 'objdump -d "{f2}" > {tempf2} ; '
213 # Remove path string inside the dissemble
214 'sed -i \'s!{rootfs1}!!g\' {tempf1} ; '
215 'sed -i \'s!{rootfs2}!!g\' {tempf2} ; '
216 'diff {tempf1} {tempf2} 1>/dev/null 2>&1').format(
217 f1=full_path1,
218 f2=full_path2,
219 rootfs1=i1.rootfs,
220 rootfs2=i2.rootfs,
221 tempf1=self.tempf1,
222 tempf2=self.tempf2)
Luis Lozano036c9232015-12-10 10:47:01 -0800223 ret = cmde.RunCommand(command, print_to_console=False)
Han Shen3dc2a882014-01-07 16:42:01 -0800224 if ret != 0:
Caroline Ticef6ef4392017-04-06 17:16:05 -0700225 self.logger.LogOutput(
226 '*** Not match - "{0}" "{1}"'.format(full_path1, full_path2))
Han Shen3dc2a882014-01-07 16:42:01 -0800227 mismatch_list.append(f1)
228 if self.diff_file:
Caroline Ticef6ef4392017-04-06 17:16:05 -0700229 command = ('echo "Diffs of disassemble of \"{f1}\" and \"{f2}\"" '
230 '>> {diff_file} ; diff {tempf1} {tempf2} '
231 '>> {diff_file}').format(
232 f1=full_path1,
233 f2=full_path2,
234 diff_file=self.diff_file,
235 tempf1=self.tempf1,
236 tempf2=self.tempf2)
Luis Lozano036c9232015-12-10 10:47:01 -0800237 cmde.RunCommand(command, print_to_console=False)
Han Shen3dc2a882014-01-07 16:42:01 -0800238 else:
239 match_count += 1
240 ## End of comparing every elf files.
241
242 if not mismatch_list:
Caroline Ticef6ef4392017-04-06 17:16:05 -0700243 self.logger.LogOutput(
244 '** COOL, ALL {0} BINARIES MATCHED!! **'.format(match_count))
Han Shen3dc2a882014-01-07 16:42:01 -0800245 return True
246
247 mismatch_str = 'Found {0} mismatch:\n'.format(len(mismatch_list))
248 for b in mismatch_list:
249 mismatch_str += '\t' + b + '\n'
250
251 self.logger.LogOutput(mismatch_str)
252 return False
253
254
255def Main(argv):
256 """The main function."""
257
258 command_executer.InitCommandExecuter()
259 images = []
260
261 parser = argparse.ArgumentParser()
262 parser.add_argument(
Caroline Ticef6ef4392017-04-06 17:16:05 -0700263 '--no_unmount',
264 action='store_true',
265 dest='no_unmount',
266 default=False,
Han Shen3dc2a882014-01-07 16:42:01 -0800267 help='Do not unmount after finish, this is useful for debugging.')
268 parser.add_argument(
Caroline Ticef6ef4392017-04-06 17:16:05 -0700269 '--chromeos_root',
270 dest='chromeos_root',
271 default=None,
272 action='store',
Han Shen3dc2a882014-01-07 16:42:01 -0800273 help=('[Optional] Specify a chromeos tree instead of '
274 'deducing it from image path so that we can compare '
275 '2 images that are downloaded.'))
276 parser.add_argument(
Caroline Ticef6ef4392017-04-06 17:16:05 -0700277 '--mount_basename',
278 dest='mount_basename',
279 default=None,
280 action='store',
Han Shen3dc2a882014-01-07 16:42:01 -0800281 help=('Specify a meaningful name for the mount point. With this being '
282 'set, the mount points would be "/tmp/mount_basename.x.rootfs" '
283 ' and "/tmp/mount_basename.x.stateful". (x is 1 or 2).'))
Caroline Ticef6ef4392017-04-06 17:16:05 -0700284 parser.add_argument(
285 '--diff_file',
286 dest='diff_file',
287 default=None,
288 help='Dumping all the diffs (if any) to the diff file')
289 parser.add_argument(
290 '--image1',
291 dest='image1',
292 default=None,
293 required=True,
294 help=('Image 1 file name.'))
295 parser.add_argument(
296 '--image2',
297 dest='image2',
298 default=None,
299 required=True,
300 help=('Image 2 file name.'))
Han Shen3dc2a882014-01-07 16:42:01 -0800301 options = parser.parse_args(argv[1:])
302
303 if options.mount_basename and options.mount_basename.find('/') >= 0:
304 logger.GetLogger().LogError(
305 '"--mount_basename" must be a name, not a path.')
306 parser.print_help()
307 return 1
308
309 result = False
310 image_comparator = None
311 try:
Caroline Tice88272d42016-01-13 09:48:29 -0800312 for i, image_path in enumerate([options.image1, options.image2], start=1):
Han Shen3dc2a882014-01-07 16:42:01 -0800313 image_path = os.path.realpath(image_path)
314 if not os.path.isfile(image_path):
315 logger.getLogger().LogError('"{0}" is not a file.'.format(image_path))
316 return 1
317
318 chromeos_root = None
319 if options.chromeos_root:
320 chromeos_root = options.chromeos_root
321 else:
322 ## Deduce chromeos root from image
323 t = image_path
324 while t != '/':
325 if misc.IsChromeOsTree(t):
326 break
327 t = os.path.dirname(t)
328 if misc.IsChromeOsTree(t):
329 chromeos_root = t
330
331 if not chromeos_root:
332 logger.GetLogger().LogError(
333 'Please provide a valid chromeos root via --chromeos_root')
334 return 1
335
336 image = CrosImage(image_path, chromeos_root, options.no_unmount)
337
338 if options.mount_basename:
339 mount_basename = '{basename}.{index}'.format(
340 basename=options.mount_basename, index=i)
341 else:
342 mount_basename = None
343
344 if image.MountImage(mount_basename):
345 images.append(image)
346 image.FindElfFiles()
347
348 if len(images) == 2:
349 image_comparator = ImageComparator(images, options.diff_file)
350 result = image_comparator.CompareImages()
351 finally:
352 for image in images:
353 image.UnmountImage()
354 if image_comparator:
355 image_comparator.Cleanup()
356
357 return 0 if result else 1
358
359
360if __name__ == '__main__':
361 Main(sys.argv)