blob: 7d9b5e34895151f05390c289d456a8e9bc430b64 [file] [log] [blame]
Han Shen3dc2a882014-01-07 16:42:01 -08001#!/usr/bin/python
2"""Diff 2 chromiumos images by comparing each elf file.
3
4 The script diffs every *ELF* files by dissembling every *executable* section,
5 which means it is not a FULL elf differ.
6
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
17__author__ = 'shenhan@google.com (Han Shen)'
18
19import argparse
20import os
21import re
22import sys
23import tempfile
24
25import image_chromeos
26from utils import command_executer
27from utils import logger
28from utils import misc
29
30
31class CrosImage(object):
32 """A cros image object."""
33
34 def __init__(self, image, chromeos_root, no_unmount):
35 self.image = image
36 self.chromeos_root = chromeos_root
37 self.mounted = False
38 self._ce = command_executer.GetCommandExecuter()
39 self.logger = logger.GetLogger()
40 self.elf_files = []
41 self.no_unmount = no_unmount
42
43 def MountImage(self, mount_basename):
44 """Mount/unpack the image."""
45
46 if mount_basename:
47 self.rootfs = '/tmp/{0}.rootfs'.format(mount_basename)
48 self.stateful = '/tmp/{0}.stateful'.format(mount_basename)
49 self.unmount_script = '/tmp/{0}.unmount.sh'.format(mount_basename)
50 else:
51 self.rootfs = tempfile.mkdtemp(suffix='.rootfs',
52 prefix='chromiumos_image_diff')
53 ## rootfs is like /tmp/tmpxyz012.rootfs.
54 match = re.match(r'^(.*)\.rootfs$', self.rootfs)
55 basename = match.group(1)
56 self.stateful = basename + '.stateful'
57 os.mkdir(self.stateful)
58 self.unmount_script = '{0}.unmount.sh'.format(basename)
59
60 self.logger.LogOutput('Mounting "{0}" onto "{1}" and "{2}"'.format(
61 self.image, self.rootfs, self.stateful))
62 ## First of all creating an unmount image
63 self.CreateUnmountScript()
64 command = image_chromeos.GetImageMountCommand(
65 self.chromeos_root, self.image, self.rootfs, self.stateful)
Luis Lozano036c9232015-12-10 10:47:01 -080066 rv = self._ce.RunCommand(command, print_to_console=True)
Han Shen3dc2a882014-01-07 16:42:01 -080067 self.mounted = (rv == 0)
68 if not self.mounted:
69 self.logger.LogError('Failed to mount "{0}" onto "{1}" and "{2}".'.format(
70 self.image, self.rootfs, self.stateful))
71 return self.mounted
72
73 def CreateUnmountScript(self):
74 command = ('sudo umount {r}/usr/local {r}/usr/share/oem '
75 '{r}/var {r}/mnt/stateful_partition {r}; sudo umount {s} ; '
76 'rmdir {r} ; rmdir {s}\n').format(r=self.rootfs, s=self.stateful)
77 f = open(self.unmount_script, 'w')
78 f.write(command)
79 f.close()
80 self._ce.RunCommand('chmod +x {}'.format(self.unmount_script),
Luis Lozano036c9232015-12-10 10:47:01 -080081 print_to_console=False)
Han Shen3dc2a882014-01-07 16:42:01 -080082 self.logger.LogOutput('Created an unmount script - "{0}"'.format(
83 self.unmount_script))
84
85 def UnmountImage(self):
86 """Unmount the image and delete mount point."""
87
88 self.logger.LogOutput('Unmounting image "{0}" from "{1}" and "{2}"'.format(
89 self.image, self.rootfs, self.stateful))
90 if self.mounted:
91 command = 'bash "{0}"'.format(self.unmount_script)
92 if self.no_unmount:
93 self.logger.LogOutput(('Please unmount manually - \n'
94 '\t bash "{0}"'.format(self.unmount_script)))
95 else:
Luis Lozano036c9232015-12-10 10:47:01 -080096 if self._ce.RunCommand(command, print_to_console=True) == 0:
Han Shen3dc2a882014-01-07 16:42:01 -080097 self._ce.RunCommand('rm {0}'.format(self.unmount_script))
98 self.mounted = False
99 self.rootfs = None
100 self.stateful = None
101 self.unmount_script = None
102
103 return not self.mounted
104
105 def FindElfFiles(self):
106 """Find all elf files for the image.
107
108 Returns:
109 Always true
110 """
111
112 self.logger.LogOutput('Finding all elf files in "{0}" ...'.format(
113 self.rootfs))
114 # Note '\;' must be prefixed by 'r'.
115 command = ('find "{0}" -type f -exec '
116 'bash -c \'file -b "{{}}" | grep -q "ELF"\'' r' \; '
117 r'-exec echo "{{}}" \;').format(self.rootfs)
118 self.logger.LogCmd(command)
Luis Lozano036c9232015-12-10 10:47:01 -0800119 _, out, _ = self._ce.RunCommandWOutput(command, print_to_console=False)
Han Shen3dc2a882014-01-07 16:42:01 -0800120 self.elf_files = out.splitlines()
121 self.logger.LogOutput(
122 'Total {0} elf files found.'.format(len(self.elf_files)))
123 return True
124
125
126class ImageComparator(object):
127 """A class that wraps comparsion actions."""
128
129 def __init__(self, images, diff_file):
130 self.images = images
131 self.logger = logger.GetLogger()
132 self.diff_file = diff_file
133 self.tempf1 = None
134 self.tempf2 = None
135
136 def Cleanup(self):
137 if self.tempf1 and self.tempf2:
138 command_executer.GetCommandExecuter().RunCommand(
139 'rm {0} {1}'.format(self.tempf1, self.tempf2))
140 logger.GetLogger('Removed "{0}" and "{1}".'.format(
141 self.tempf1, self.tempf2))
142
143 def CheckElfFileSetEquality(self):
144 """Checking whether images have exactly number of elf files."""
145
146 self.logger.LogOutput('Checking elf file equality ...')
147 i1 = self.images[0]
148 i2 = self.images[1]
149 t1 = i1.rootfs + '/'
150 elfset1 = set([e.replace(t1, '') for e in i1.elf_files])
151 t2 = i2.rootfs + '/'
152 elfset2 = set([e.replace(t2, '') for e in i2.elf_files])
153 dif1 = elfset1.difference(elfset2)
154 msg = None
155 if dif1:
156 msg = 'The following files are not in "{image}" - "{rootfs}":\n'.format(
157 image=i2.image, rootfs=i2.rootfs)
158 for d in dif1:
159 msg += '\t' + d + '\n'
160 dif2 = elfset2.difference(elfset1)
161 if dif2:
162 msg = 'The following files are not in "{image}" - "{rootfs}":\n'.format(
163 image=i1.image, rootfs=i1.rootfs)
164 for d in dif2:
165 msg += '\t' + d + '\n'
166 if msg:
167 self.logger.LogError(msg)
168 return False
169 return True
170
171 def CompareImages(self):
172 """Do the comparsion work."""
173
174 if not self.CheckElfFileSetEquality():
175 return False
176
177 mismatch_list = []
178 match_count = 0
179 i1 = self.images[0]
180 i2 = self.images[1]
181 self.logger.LogOutput('Start comparing {0} elf file by file ...'.format(
182 len(i1.elf_files)))
183 ## Note - i1.elf_files and i2.elf_files have exactly the same entries here.
184
185 ## Create 2 temp files to be used for all disassembed files.
186 handle, self.tempf1 = tempfile.mkstemp()
187 os.close(handle) # We do not need the handle
188 handle, self.tempf2 = tempfile.mkstemp()
189 os.close(handle)
190
191 cmde = command_executer.GetCommandExecuter()
192 for elf1 in i1.elf_files:
193 tmp_rootfs = i1.rootfs + '/'
194 f1 = elf1.replace(tmp_rootfs, '')
195 full_path1 = elf1
196 full_path2 = elf1.replace(i1.rootfs, i2.rootfs)
197
198 if full_path1 == full_path2:
199 self.logger.LogError(
200 'Error: We\'re comparing the SAME file - {0}'.format(f1))
201 continue
202
203 command = ('objdump -d "{f1}" > {tempf1} ; '
204 'objdump -d "{f2}" > {tempf2} ; '
205 # Remove path string inside the dissemble
206 'sed -i \'s!{rootfs1}!!g\' {tempf1} ; '
207 'sed -i \'s!{rootfs2}!!g\' {tempf2} ; '
208 'diff {tempf1} {tempf2} 1>/dev/null 2>&1').format(
209 f1=full_path1, f2=full_path2,
210 rootfs1=i1.rootfs, rootfs2=i2.rootfs,
211 tempf1=self.tempf1, tempf2=self.tempf2)
Luis Lozano036c9232015-12-10 10:47:01 -0800212 ret = cmde.RunCommand(command, print_to_console=False)
Han Shen3dc2a882014-01-07 16:42:01 -0800213 if ret != 0:
214 self.logger.LogOutput('*** Not match - "{0}" "{1}"'.format(
215 full_path1, full_path2))
216 mismatch_list.append(f1)
217 if self.diff_file:
218 command = (
219 'echo "Diffs of disassemble of \"{f1}\" and \"{f2}\"" '
220 '>> {diff_file} ; diff {tempf1} {tempf2} '
221 '>> {diff_file}').format(
222 f1=full_path1, f2=full_path2, diff_file=self.diff_file,
223 tempf1=self.tempf1, tempf2=self.tempf2)
Luis Lozano036c9232015-12-10 10:47:01 -0800224 cmde.RunCommand(command, print_to_console=False)
Han Shen3dc2a882014-01-07 16:42:01 -0800225 else:
226 match_count += 1
227 ## End of comparing every elf files.
228
229 if not mismatch_list:
230 self.logger.LogOutput('** COOL, ALL {0} BINARIES MATCHED!! **'.format(
231 match_count))
232 return True
233
234 mismatch_str = 'Found {0} mismatch:\n'.format(len(mismatch_list))
235 for b in mismatch_list:
236 mismatch_str += '\t' + b + '\n'
237
238 self.logger.LogOutput(mismatch_str)
239 return False
240
241
242def Main(argv):
243 """The main function."""
244
245 command_executer.InitCommandExecuter()
246 images = []
247
248 parser = argparse.ArgumentParser()
249 parser.add_argument(
250 '--no_unmount', action='store_true', dest='no_unmount', default=False,
251 help='Do not unmount after finish, this is useful for debugging.')
252 parser.add_argument(
253 '--chromeos_root', dest='chromeos_root', default=None, action='store',
254 help=('[Optional] Specify a chromeos tree instead of '
255 'deducing it from image path so that we can compare '
256 '2 images that are downloaded.'))
257 parser.add_argument(
258 '--mount_basename', dest='mount_basename', default=None, action='store',
259 help=('Specify a meaningful name for the mount point. With this being '
260 'set, the mount points would be "/tmp/mount_basename.x.rootfs" '
261 ' and "/tmp/mount_basename.x.stateful". (x is 1 or 2).'))
262 parser.add_argument('--diff_file', dest='diff_file', default=None,
263 help='Dumping all the diffs (if any) to the diff file')
264 parser.add_argument('--image1', dest='image1', default=None,
265 required=True, help=('Image 1 file name.'))
266 parser.add_argument('--image2', dest='image2', default=None,
267 required=True, help=('Image 2 file name.'))
268 options = parser.parse_args(argv[1:])
269
270 if options.mount_basename and options.mount_basename.find('/') >= 0:
271 logger.GetLogger().LogError(
272 '"--mount_basename" must be a name, not a path.')
273 parser.print_help()
274 return 1
275
276 result = False
277 image_comparator = None
278 try:
279 for image_path in [options.image1, options.image2]:
280 image_path = os.path.realpath(image_path)
281 if not os.path.isfile(image_path):
282 logger.getLogger().LogError('"{0}" is not a file.'.format(image_path))
283 return 1
284
285 chromeos_root = None
286 if options.chromeos_root:
287 chromeos_root = options.chromeos_root
288 else:
289 ## Deduce chromeos root from image
290 t = image_path
291 while t != '/':
292 if misc.IsChromeOsTree(t):
293 break
294 t = os.path.dirname(t)
295 if misc.IsChromeOsTree(t):
296 chromeos_root = t
297
298 if not chromeos_root:
299 logger.GetLogger().LogError(
300 'Please provide a valid chromeos root via --chromeos_root')
301 return 1
302
303 image = CrosImage(image_path, chromeos_root, options.no_unmount)
304
305 if options.mount_basename:
306 mount_basename = '{basename}.{index}'.format(
307 basename=options.mount_basename, index=i)
308 else:
309 mount_basename = None
310
311 if image.MountImage(mount_basename):
312 images.append(image)
313 image.FindElfFiles()
314
315 if len(images) == 2:
316 image_comparator = ImageComparator(images, options.diff_file)
317 result = image_comparator.CompareImages()
318 finally:
319 for image in images:
320 image.UnmountImage()
321 if image_comparator:
322 image_comparator.Cleanup()
323
324 return 0 if result else 1
325
326
327if __name__ == '__main__':
328 Main(sys.argv)