xref: /aosp_15_r20/external/toolchain-utils/chromiumos_image_diff.py (revision 760c253c1ed00ce9abd48f8546f08516e57485fe)
1*760c253cSXin Li#!/usr/bin/env python3
2*760c253cSXin Li# -*- coding: utf-8 -*-
3*760c253cSXin Li#
4*760c253cSXin Li# Copyright 2019 The ChromiumOS Authors
5*760c253cSXin Li# Use of this source code is governed by a BSD-style license that can be
6*760c253cSXin Li# found in the LICENSE file.
7*760c253cSXin Li
8*760c253cSXin Li"""Diff 2 chromiumos images by comparing each elf file.
9*760c253cSXin Li
10*760c253cSXin Li   The script diffs every *ELF* files by dissembling every *executable*
11*760c253cSXin Li   section, which means it is not a FULL elf differ.
12*760c253cSXin Li
13*760c253cSXin Li   A simple usage example -
14*760c253cSXin Li     chromiumos_image_diff.py --image1 image-path-1 --image2 image-path-2
15*760c253cSXin Li
16*760c253cSXin Li   Note that image path should be inside the chroot, if not (ie, image is
17*760c253cSXin Li   downloaded from web), please specify a chromiumos checkout via
18*760c253cSXin Li   "--chromeos_root".
19*760c253cSXin Li
20*760c253cSXin Li   And this script should be executed outside chroot.
21*760c253cSXin Li"""
22*760c253cSXin Li
23*760c253cSXin Li
24*760c253cSXin Li__author__ = "[email protected] (Han Shen)"
25*760c253cSXin Li
26*760c253cSXin Liimport argparse
27*760c253cSXin Liimport os
28*760c253cSXin Liimport re
29*760c253cSXin Liimport sys
30*760c253cSXin Liimport tempfile
31*760c253cSXin Li
32*760c253cSXin Lifrom cros_utils import command_executer
33*760c253cSXin Lifrom cros_utils import logger
34*760c253cSXin Lifrom cros_utils import misc
35*760c253cSXin Liimport image_chromeos
36*760c253cSXin Li
37*760c253cSXin Li
38*760c253cSXin Liclass CrosImage(object):
39*760c253cSXin Li    """A cros image object."""
40*760c253cSXin Li
41*760c253cSXin Li    def __init__(self, image, chromeos_root, no_unmount):
42*760c253cSXin Li        self.image = image
43*760c253cSXin Li        self.chromeos_root = chromeos_root
44*760c253cSXin Li        self.mounted = False
45*760c253cSXin Li        self._ce = command_executer.GetCommandExecuter()
46*760c253cSXin Li        self.logger = logger.GetLogger()
47*760c253cSXin Li        self.elf_files = []
48*760c253cSXin Li        self.no_unmount = no_unmount
49*760c253cSXin Li        self.unmount_script = ""
50*760c253cSXin Li        self.stateful = ""
51*760c253cSXin Li        self.rootfs = ""
52*760c253cSXin Li
53*760c253cSXin Li    def MountImage(self, mount_basename):
54*760c253cSXin Li        """Mount/unpack the image."""
55*760c253cSXin Li
56*760c253cSXin Li        if mount_basename:
57*760c253cSXin Li            self.rootfs = "/tmp/{0}.rootfs".format(mount_basename)
58*760c253cSXin Li            self.stateful = "/tmp/{0}.stateful".format(mount_basename)
59*760c253cSXin Li            self.unmount_script = "/tmp/{0}.unmount.sh".format(mount_basename)
60*760c253cSXin Li        else:
61*760c253cSXin Li            self.rootfs = tempfile.mkdtemp(
62*760c253cSXin Li                suffix=".rootfs", prefix="chromiumos_image_diff"
63*760c253cSXin Li            )
64*760c253cSXin Li            ## rootfs is like /tmp/tmpxyz012.rootfs.
65*760c253cSXin Li            match = re.match(r"^(.*)\.rootfs$", self.rootfs)
66*760c253cSXin Li            basename = match.group(1)
67*760c253cSXin Li            self.stateful = basename + ".stateful"
68*760c253cSXin Li            os.mkdir(self.stateful)
69*760c253cSXin Li            self.unmount_script = "{0}.unmount.sh".format(basename)
70*760c253cSXin Li
71*760c253cSXin Li        self.logger.LogOutput(
72*760c253cSXin Li            'Mounting "{0}" onto "{1}" and "{2}"'.format(
73*760c253cSXin Li                self.image, self.rootfs, self.stateful
74*760c253cSXin Li            )
75*760c253cSXin Li        )
76*760c253cSXin Li        ## First of all creating an unmount image
77*760c253cSXin Li        self.CreateUnmountScript()
78*760c253cSXin Li        command = image_chromeos.GetImageMountCommand(
79*760c253cSXin Li            self.image, self.rootfs, self.stateful
80*760c253cSXin Li        )
81*760c253cSXin Li        rv = self._ce.RunCommand(command, print_to_console=True)
82*760c253cSXin Li        self.mounted = rv == 0
83*760c253cSXin Li        if not self.mounted:
84*760c253cSXin Li            self.logger.LogError(
85*760c253cSXin Li                'Failed to mount "{0}" onto "{1}" and "{2}".'.format(
86*760c253cSXin Li                    self.image, self.rootfs, self.stateful
87*760c253cSXin Li                )
88*760c253cSXin Li            )
89*760c253cSXin Li        return self.mounted
90*760c253cSXin Li
91*760c253cSXin Li    def CreateUnmountScript(self):
92*760c253cSXin Li        command = (
93*760c253cSXin Li            "sudo umount {r}/usr/local {r}/usr/share/oem "
94*760c253cSXin Li            "{r}/var {r}/mnt/stateful_partition {r}; sudo umount {s} ; "
95*760c253cSXin Li            "rmdir {r} ; rmdir {s}\n"
96*760c253cSXin Li        ).format(r=self.rootfs, s=self.stateful)
97*760c253cSXin Li        f = open(self.unmount_script, "w", encoding="utf-8")
98*760c253cSXin Li        f.write(command)
99*760c253cSXin Li        f.close()
100*760c253cSXin Li        self._ce.RunCommand(
101*760c253cSXin Li            "chmod +x {}".format(self.unmount_script), print_to_console=False
102*760c253cSXin Li        )
103*760c253cSXin Li        self.logger.LogOutput(
104*760c253cSXin Li            'Created an unmount script - "{0}"'.format(self.unmount_script)
105*760c253cSXin Li        )
106*760c253cSXin Li
107*760c253cSXin Li    def UnmountImage(self):
108*760c253cSXin Li        """Unmount the image and delete mount point."""
109*760c253cSXin Li
110*760c253cSXin Li        self.logger.LogOutput(
111*760c253cSXin Li            'Unmounting image "{0}" from "{1}" and "{2}"'.format(
112*760c253cSXin Li                self.image, self.rootfs, self.stateful
113*760c253cSXin Li            )
114*760c253cSXin Li        )
115*760c253cSXin Li        if self.mounted:
116*760c253cSXin Li            command = 'bash "{0}"'.format(self.unmount_script)
117*760c253cSXin Li            if self.no_unmount:
118*760c253cSXin Li                self.logger.LogOutput(
119*760c253cSXin Li                    (
120*760c253cSXin Li                        "Please unmount manually - \n"
121*760c253cSXin Li                        '\t bash "{0}"'.format(self.unmount_script)
122*760c253cSXin Li                    )
123*760c253cSXin Li                )
124*760c253cSXin Li            else:
125*760c253cSXin Li                if self._ce.RunCommand(command, print_to_console=True) == 0:
126*760c253cSXin Li                    self._ce.RunCommand("rm {0}".format(self.unmount_script))
127*760c253cSXin Li                    self.mounted = False
128*760c253cSXin Li                    self.rootfs = None
129*760c253cSXin Li                    self.stateful = None
130*760c253cSXin Li                    self.unmount_script = None
131*760c253cSXin Li
132*760c253cSXin Li        return not self.mounted
133*760c253cSXin Li
134*760c253cSXin Li    def FindElfFiles(self):
135*760c253cSXin Li        """Find all elf files for the image.
136*760c253cSXin Li
137*760c253cSXin Li        Returns:
138*760c253cSXin Li          Always true
139*760c253cSXin Li        """
140*760c253cSXin Li
141*760c253cSXin Li        self.logger.LogOutput(
142*760c253cSXin Li            'Finding all elf files in "{0}" ...'.format(self.rootfs)
143*760c253cSXin Li        )
144*760c253cSXin Li        # Note '\;' must be prefixed by 'r'.
145*760c253cSXin Li        command = (
146*760c253cSXin Li            'find "{0}" -type f -exec '
147*760c253cSXin Li            'bash -c \'file -b "{{}}" | grep -q "ELF"\''
148*760c253cSXin Li            r" \; "
149*760c253cSXin Li            r'-exec echo "{{}}" \;'
150*760c253cSXin Li        ).format(self.rootfs)
151*760c253cSXin Li        self.logger.LogCmd(command)
152*760c253cSXin Li        _, out, _ = self._ce.RunCommandWOutput(command, print_to_console=False)
153*760c253cSXin Li        self.elf_files = out.splitlines()
154*760c253cSXin Li        self.logger.LogOutput(
155*760c253cSXin Li            "Total {0} elf files found.".format(len(self.elf_files))
156*760c253cSXin Li        )
157*760c253cSXin Li        return True
158*760c253cSXin Li
159*760c253cSXin Li
160*760c253cSXin Liclass ImageComparator(object):
161*760c253cSXin Li    """A class that wraps comparsion actions."""
162*760c253cSXin Li
163*760c253cSXin Li    def __init__(self, images, diff_file):
164*760c253cSXin Li        self.images = images
165*760c253cSXin Li        self.logger = logger.GetLogger()
166*760c253cSXin Li        self.diff_file = diff_file
167*760c253cSXin Li        self.tempf1 = None
168*760c253cSXin Li        self.tempf2 = None
169*760c253cSXin Li
170*760c253cSXin Li    def Cleanup(self):
171*760c253cSXin Li        if self.tempf1 and self.tempf2:
172*760c253cSXin Li            command_executer.GetCommandExecuter().RunCommand(
173*760c253cSXin Li                "rm {0} {1}".format(self.tempf1, self.tempf2)
174*760c253cSXin Li            )
175*760c253cSXin Li            logger.GetLogger(
176*760c253cSXin Li                'Removed "{0}" and "{1}".'.format(self.tempf1, self.tempf2)
177*760c253cSXin Li            )
178*760c253cSXin Li
179*760c253cSXin Li    def CheckElfFileSetEquality(self):
180*760c253cSXin Li        """Checking whether images have exactly number of elf files."""
181*760c253cSXin Li
182*760c253cSXin Li        self.logger.LogOutput("Checking elf file equality ...")
183*760c253cSXin Li        i1 = self.images[0]
184*760c253cSXin Li        i2 = self.images[1]
185*760c253cSXin Li        t1 = i1.rootfs + "/"
186*760c253cSXin Li        elfset1 = {e.replace(t1, "") for e in i1.elf_files}
187*760c253cSXin Li        t2 = i2.rootfs + "/"
188*760c253cSXin Li        elfset2 = {e.replace(t2, "") for e in i2.elf_files}
189*760c253cSXin Li        dif1 = elfset1.difference(elfset2)
190*760c253cSXin Li        msg = None
191*760c253cSXin Li        if dif1:
192*760c253cSXin Li            msg = 'The following files are not in "{image}" - "{rootfs}":\n'.format(
193*760c253cSXin Li                image=i2.image, rootfs=i2.rootfs
194*760c253cSXin Li            )
195*760c253cSXin Li            for d in dif1:
196*760c253cSXin Li                msg += "\t" + d + "\n"
197*760c253cSXin Li        dif2 = elfset2.difference(elfset1)
198*760c253cSXin Li        if dif2:
199*760c253cSXin Li            msg = 'The following files are not in "{image}" - "{rootfs}":\n'.format(
200*760c253cSXin Li                image=i1.image, rootfs=i1.rootfs
201*760c253cSXin Li            )
202*760c253cSXin Li            for d in dif2:
203*760c253cSXin Li                msg += "\t" + d + "\n"
204*760c253cSXin Li        if msg:
205*760c253cSXin Li            self.logger.LogError(msg)
206*760c253cSXin Li            return False
207*760c253cSXin Li        return True
208*760c253cSXin Li
209*760c253cSXin Li    def CompareImages(self):
210*760c253cSXin Li        """Do the comparsion work."""
211*760c253cSXin Li
212*760c253cSXin Li        if not self.CheckElfFileSetEquality():
213*760c253cSXin Li            return False
214*760c253cSXin Li
215*760c253cSXin Li        mismatch_list = []
216*760c253cSXin Li        match_count = 0
217*760c253cSXin Li        i1 = self.images[0]
218*760c253cSXin Li        i2 = self.images[1]
219*760c253cSXin Li        self.logger.LogOutput(
220*760c253cSXin Li            "Start comparing {0} elf file by file ...".format(len(i1.elf_files))
221*760c253cSXin Li        )
222*760c253cSXin Li        ## Note - i1.elf_files and i2.elf_files have exactly the same entries here.
223*760c253cSXin Li
224*760c253cSXin Li        ## Create 2 temp files to be used for all disassembed files.
225*760c253cSXin Li        handle, self.tempf1 = tempfile.mkstemp()
226*760c253cSXin Li        os.close(handle)  # We do not need the handle
227*760c253cSXin Li        handle, self.tempf2 = tempfile.mkstemp()
228*760c253cSXin Li        os.close(handle)
229*760c253cSXin Li
230*760c253cSXin Li        cmde = command_executer.GetCommandExecuter()
231*760c253cSXin Li        for elf1 in i1.elf_files:
232*760c253cSXin Li            tmp_rootfs = i1.rootfs + "/"
233*760c253cSXin Li            f1 = elf1.replace(tmp_rootfs, "")
234*760c253cSXin Li            full_path1 = elf1
235*760c253cSXin Li            full_path2 = elf1.replace(i1.rootfs, i2.rootfs)
236*760c253cSXin Li
237*760c253cSXin Li            if full_path1 == full_path2:
238*760c253cSXin Li                self.logger.LogError(
239*760c253cSXin Li                    "Error:  We're comparing the SAME file - {0}".format(f1)
240*760c253cSXin Li                )
241*760c253cSXin Li                continue
242*760c253cSXin Li
243*760c253cSXin Li            command = (
244*760c253cSXin Li                'objdump -d "{f1}" > {tempf1} ; '
245*760c253cSXin Li                'objdump -d "{f2}" > {tempf2} ; '
246*760c253cSXin Li                # Remove path string inside the dissemble
247*760c253cSXin Li                "sed -i 's!{rootfs1}!!g' {tempf1} ; "
248*760c253cSXin Li                "sed -i 's!{rootfs2}!!g' {tempf2} ; "
249*760c253cSXin Li                "diff {tempf1} {tempf2} 1>/dev/null 2>&1"
250*760c253cSXin Li            ).format(
251*760c253cSXin Li                f1=full_path1,
252*760c253cSXin Li                f2=full_path2,
253*760c253cSXin Li                rootfs1=i1.rootfs,
254*760c253cSXin Li                rootfs2=i2.rootfs,
255*760c253cSXin Li                tempf1=self.tempf1,
256*760c253cSXin Li                tempf2=self.tempf2,
257*760c253cSXin Li            )
258*760c253cSXin Li            ret = cmde.RunCommand(command, print_to_console=False)
259*760c253cSXin Li            if ret != 0:
260*760c253cSXin Li                self.logger.LogOutput(
261*760c253cSXin Li                    '*** Not match - "{0}" "{1}"'.format(full_path1, full_path2)
262*760c253cSXin Li                )
263*760c253cSXin Li                mismatch_list.append(f1)
264*760c253cSXin Li                if self.diff_file:
265*760c253cSXin Li                    command = (
266*760c253cSXin Li                        'echo "Diffs of disassemble of "{f1}" and "{f2}"" '
267*760c253cSXin Li                        ">> {diff_file} ; diff {tempf1} {tempf2} "
268*760c253cSXin Li                        ">> {diff_file}"
269*760c253cSXin Li                    ).format(
270*760c253cSXin Li                        f1=full_path1,
271*760c253cSXin Li                        f2=full_path2,
272*760c253cSXin Li                        diff_file=self.diff_file,
273*760c253cSXin Li                        tempf1=self.tempf1,
274*760c253cSXin Li                        tempf2=self.tempf2,
275*760c253cSXin Li                    )
276*760c253cSXin Li                    cmde.RunCommand(command, print_to_console=False)
277*760c253cSXin Li            else:
278*760c253cSXin Li                match_count += 1
279*760c253cSXin Li        ## End of comparing every elf files.
280*760c253cSXin Li
281*760c253cSXin Li        if not mismatch_list:
282*760c253cSXin Li            self.logger.LogOutput(
283*760c253cSXin Li                "** COOL, ALL {0} BINARIES MATCHED!! **".format(match_count)
284*760c253cSXin Li            )
285*760c253cSXin Li            return True
286*760c253cSXin Li
287*760c253cSXin Li        mismatch_str = "Found {0} mismatch:\n".format(len(mismatch_list))
288*760c253cSXin Li        for b in mismatch_list:
289*760c253cSXin Li            mismatch_str += "\t" + b + "\n"
290*760c253cSXin Li
291*760c253cSXin Li        self.logger.LogOutput(mismatch_str)
292*760c253cSXin Li        return False
293*760c253cSXin Li
294*760c253cSXin Li
295*760c253cSXin Lidef Main(argv):
296*760c253cSXin Li    """The main function."""
297*760c253cSXin Li
298*760c253cSXin Li    command_executer.InitCommandExecuter()
299*760c253cSXin Li    images = []
300*760c253cSXin Li
301*760c253cSXin Li    parser = argparse.ArgumentParser()
302*760c253cSXin Li    parser.add_argument(
303*760c253cSXin Li        "--no_unmount",
304*760c253cSXin Li        action="store_true",
305*760c253cSXin Li        dest="no_unmount",
306*760c253cSXin Li        default=False,
307*760c253cSXin Li        help="Do not unmount after finish, this is useful for debugging.",
308*760c253cSXin Li    )
309*760c253cSXin Li    parser.add_argument(
310*760c253cSXin Li        "--chromeos_root",
311*760c253cSXin Li        dest="chromeos_root",
312*760c253cSXin Li        default=None,
313*760c253cSXin Li        action="store",
314*760c253cSXin Li        help=(
315*760c253cSXin Li            "[Optional] Specify a chromeos tree instead of "
316*760c253cSXin Li            "deducing it from image path so that we can compare "
317*760c253cSXin Li            "2 images that are downloaded."
318*760c253cSXin Li        ),
319*760c253cSXin Li    )
320*760c253cSXin Li    parser.add_argument(
321*760c253cSXin Li        "--mount_basename",
322*760c253cSXin Li        dest="mount_basename",
323*760c253cSXin Li        default=None,
324*760c253cSXin Li        action="store",
325*760c253cSXin Li        help=(
326*760c253cSXin Li            "Specify a meaningful name for the mount point. With this being "
327*760c253cSXin Li            'set, the mount points would be "/tmp/mount_basename.x.rootfs" '
328*760c253cSXin Li            ' and "/tmp/mount_basename.x.stateful". (x is 1 or 2).'
329*760c253cSXin Li        ),
330*760c253cSXin Li    )
331*760c253cSXin Li    parser.add_argument(
332*760c253cSXin Li        "--diff_file",
333*760c253cSXin Li        dest="diff_file",
334*760c253cSXin Li        default=None,
335*760c253cSXin Li        help="Dumping all the diffs (if any) to the diff file",
336*760c253cSXin Li    )
337*760c253cSXin Li    parser.add_argument(
338*760c253cSXin Li        "--image1",
339*760c253cSXin Li        dest="image1",
340*760c253cSXin Li        default=None,
341*760c253cSXin Li        required=True,
342*760c253cSXin Li        help=("Image 1 file name."),
343*760c253cSXin Li    )
344*760c253cSXin Li    parser.add_argument(
345*760c253cSXin Li        "--image2",
346*760c253cSXin Li        dest="image2",
347*760c253cSXin Li        default=None,
348*760c253cSXin Li        required=True,
349*760c253cSXin Li        help=("Image 2 file name."),
350*760c253cSXin Li    )
351*760c253cSXin Li    options = parser.parse_args(argv[1:])
352*760c253cSXin Li
353*760c253cSXin Li    if options.mount_basename and options.mount_basename.find("/") >= 0:
354*760c253cSXin Li        logger.GetLogger().LogError(
355*760c253cSXin Li            '"--mount_basename" must be a name, not a path.'
356*760c253cSXin Li        )
357*760c253cSXin Li        parser.print_help()
358*760c253cSXin Li        return 1
359*760c253cSXin Li
360*760c253cSXin Li    result = False
361*760c253cSXin Li    image_comparator = None
362*760c253cSXin Li    try:
363*760c253cSXin Li        for i, image_path in enumerate(
364*760c253cSXin Li            [options.image1, options.image2], start=1
365*760c253cSXin Li        ):
366*760c253cSXin Li            image_path = os.path.realpath(image_path)
367*760c253cSXin Li            if not os.path.isfile(image_path):
368*760c253cSXin Li                logger.GetLogger().LogError(
369*760c253cSXin Li                    '"{0}" is not a file.'.format(image_path)
370*760c253cSXin Li                )
371*760c253cSXin Li                return 1
372*760c253cSXin Li
373*760c253cSXin Li            chromeos_root = None
374*760c253cSXin Li            if options.chromeos_root:
375*760c253cSXin Li                chromeos_root = options.chromeos_root
376*760c253cSXin Li            else:
377*760c253cSXin Li                ## Deduce chromeos root from image
378*760c253cSXin Li                t = image_path
379*760c253cSXin Li                while t != "/":
380*760c253cSXin Li                    if misc.IsChromeOsTree(t):
381*760c253cSXin Li                        break
382*760c253cSXin Li                    t = os.path.dirname(t)
383*760c253cSXin Li                if misc.IsChromeOsTree(t):
384*760c253cSXin Li                    chromeos_root = t
385*760c253cSXin Li
386*760c253cSXin Li            if not chromeos_root:
387*760c253cSXin Li                logger.GetLogger().LogError(
388*760c253cSXin Li                    "Please provide a valid chromeos root via --chromeos_root"
389*760c253cSXin Li                )
390*760c253cSXin Li                return 1
391*760c253cSXin Li
392*760c253cSXin Li            image = CrosImage(image_path, chromeos_root, options.no_unmount)
393*760c253cSXin Li
394*760c253cSXin Li            if options.mount_basename:
395*760c253cSXin Li                mount_basename = "{basename}.{index}".format(
396*760c253cSXin Li                    basename=options.mount_basename, index=i
397*760c253cSXin Li                )
398*760c253cSXin Li            else:
399*760c253cSXin Li                mount_basename = None
400*760c253cSXin Li
401*760c253cSXin Li            if image.MountImage(mount_basename):
402*760c253cSXin Li                images.append(image)
403*760c253cSXin Li                image.FindElfFiles()
404*760c253cSXin Li
405*760c253cSXin Li        if len(images) == 2:
406*760c253cSXin Li            image_comparator = ImageComparator(images, options.diff_file)
407*760c253cSXin Li            result = image_comparator.CompareImages()
408*760c253cSXin Li    finally:
409*760c253cSXin Li        for image in images:
410*760c253cSXin Li            image.UnmountImage()
411*760c253cSXin Li        if image_comparator:
412*760c253cSXin Li            image_comparator.Cleanup()
413*760c253cSXin Li
414*760c253cSXin Li    return 0 if result else 1
415*760c253cSXin Li
416*760c253cSXin Li
417*760c253cSXin Liif __name__ == "__main__":
418*760c253cSXin Li    Main(sys.argv)
419