xref: /aosp_15_r20/build/bazel/scripts/apex_compare.sh (revision 7594170e27e0732bc44b93d1440d87a54b6ffe7c)
1#! /bin/bash -eu
2
3# Compares two APEX files.
4# This script is aimed at regression testing. It allows to compare an
5# APEX target built by Bazel to the same target built by Soong.
6# The first of its arguments is the reference APEX (the one built by
7# Soong), the second is "our" APEX (built by Bazel).
8#
9# An APEX is a ZIP archive, so we treat each APEX as a file system and
10# compare these two file systems. The script displays:
11#  - missing files (those in the reference APEX missing from our APEX)
12#  - extra files (those only in our APEX)
13#  - for each file present in both, their difference.
14# The main part of an APEX is an image file (payload.img), which is an
15# image of a filesystem in EXT2 format. The script "mounts" such image
16# and then compares them side by side.
17#
18# This script relies on the presence of an executable (binary/script)
19# to "mount" a file of certain formats as file systems. It runs this
20# executable as follows:
21# * mount ZIPFILE at DIR:
22#    view_file_as_fs zip ZIPFILE DIR
23# * unmount ZIPFILE at DIR:
24#    view_file_as_fs -u zip DIR
25# * mount EXT2 image IMGFILE at DIR:
26#    view_file_as_fs ext2 IMGFILE DIR
27# * unmount EXT2 image IMGFILE at DIR:
28#    view_file_as_fs -u ext2 DIR
29#
30
31function die() { format=$1; shift; printf "$format\n" $@; exit 1; }
32
33# Delouse
34(($# == 2)) || die "usage: ${0##*/} REF_APEX OUR_APEX"
35declare -r ref_apex=$1 our_apex=$2
36for f in $ref_apex $our_apex; do
37    [[ -f $f ]] || die "$f does not exist"
38done
39
40# Maybe we are lucky.
41cmp -s $ref_apex $our_apex && exit
42
43declare -r file_as_fs_viewer=$(which view_file_as_fs)
44if [[ -z "${file_as_fs_viewer}" ]]; then
45    cat <<"EOF"
46You need to have file-as-filesystem viewer application `view_file_as_fs`
47on the PATH. If you have FUSE's fuse-ext2 and fuse-zip installed, you
48can the following script below view_file_as_fs:
49
50#!/bin/bash -eu
51#
52# Mounts a file as a read-only filesystem or unmounts such previously
53# mounted file system.
54# This script can mount a zip file or an file containing an ext2 image
55# as a file system. It requires the presence of fuse-zip and fuse-ext2
56# FUSE packages.
57function die() { format=$1; shift; printf "$format\n" $@; exit 1; }
58function usage() {
59    die "Usage:\n ${0##*/} {ext2|zip} FILE MOUNT-POINT\nor\n ${0##*/} -u {ext2|zip} MOUNT-POINT"
60}
61
62declare umount=
63while getopts "u" opt; do
64    case $opt in
65      u) umount=t ;;
66      ?) usage
67    esac
68done
69
70shift $(($OPTIND-1))
71if [[ -n "$umount" ]]; then
72    (($#==2)) || usage
73    mount | grep -q "on $2 " && umount "$2"
74else
75    (($#==3)) || usage
76    declare -r file="$2" mt="$3"
77    [[ -d "$mt" && -z "$(ls -1A $mt)" ]] || die "$mt should be an empty directory"
78    case "$1" in
79        ext2) fuse-ext2 "$file" "$mt" ;;
80        zip)
81            [[ -f $file ]] || die "$file is not a file"  # Because fuse-zip silently mounts it as empty
82            fuse-zip "$file" "$mt" ;;
83        *) usage ;;
84    esac
85fi
86EOF
87    exit 1
88fi
89
90# "Mounts" file as filesystem and prints the sorted list of files in it.
91function mount_and_list() {
92    $file_as_fs_viewer $1 $2 $3 2>/dev/null
93    find $3 -type f -printf "%P\n"
94}
95
96function cleanup() {
97    for d in $fuse_dir/*.img; do
98        $file_as_fs_viewer -u ext2 $d || /bin/true
99    done
100    for d in $fuse_dir/*.apex; do
101        $file_as_fs_viewer -u zip $d || /bin/true
102    done
103    rm -rf $fuse_dir
104}
105
106function dump_proto() {
107    protoc --decode $1 $2
108}
109
110function dump_buildinfo() {
111    dump_proto apex.proto.ApexBuildInfo system/apex/proto/apex_build_info.proto
112}
113
114function dump_apex_manifest() {
115    dump_proto apex.proto.ApexManifest system/apex/proto/apex_manifest.proto
116}
117
118function compare_images() {
119    local -r ref_img=$1 our_img=$2
120
121    # Mount each APEX and save its sorted contents. Classify the contents
122    mount_and_list ext2 $ref_img $fuse_dir/ref.img >$fuse_dir/ref.img.list
123    mount_and_list ext2 $our_img $fuse_dir/our.img >$fuse_dir/our.img.list
124    . <(classify $fuse_dir/ref.img.list $fuse_dir/our.img.list; /bin/true)
125
126    # Now we have missing/extra/common holding respective file lists. Compare
127    ((${#missing[@]}==0)) || \
128      { printf "Missing image files:"; printf " %s" ${missing[@]}; printf "\n"; }
129    ((${#extra[@]}==0)) || \
130      { printf "Extra image files:"; printf " %s" ${extra[@]}; printf "\n"; }
131    for f in "${common[@]}"; do
132        cmp -s $fuse_dir/{ref,our}.img/$f && continue
133        echo "    $f" in image differs:
134        case $f in
135            etc/init.rc)
136                diff $fuse_dir/{ref,our}.img/$f || /bin/true
137                ;;
138            apex_manifest.pb)
139                diff <(dump_apex_manifest <$fuse_dir/ref.img/$f) <(dump_apex_manifest <$fuse_dir/our.img/$f) || bin/true
140                ;;
141            *)
142                # TODO: should do more than just size comparison.
143                sizes=($(stat --format "%s" $fuse_dir/{ref,our}.img/$f))
144                delta=$((${sizes[1]}-${sizes[0]}))
145                (($delta==0)) || printf "      size differs: %d (%d)\n" ${sizes[1]} $delta
146                ;;
147        esac
148    done
149}
150
151# Prints the script that sets `missing`/`extra`/`common` shell
152# variable to an array containing corresponding files, i.e. its
153# output is
154#   declare declare -a missing=() extra=() common=()
155#   missing+=(missing_file)
156#   extra+=(extra_file)
157#   common+=(common_file)
158#   .....
159function classify() {
160    comm $1 $2 | sed -nr \
161      -e '1ideclare -a missing=() extra=() common=()' \
162      -e '/^\t\t/{s/\t\t(.*)/common+=(\1)/p;d}' \
163      -e '/^\t/{s/^\t(.*)/extra+=(\1)/p;d}' \
164      -e 's/(.*)/missing+=(\1)/p'; /bin/true
165}
166
167fuse_dir=$(mktemp -d --tmpdir apexfuse.XXXXX)
168mkdir -p $fuse_dir/{our,ref}.{apex,img}
169trap cleanup EXIT
170
171# Mount each APEX and save its sorted contents. Classify the contents
172mount_and_list zip $ref_apex $fuse_dir/ref.apex >$fuse_dir/ref.apex.list
173mount_and_list zip $our_apex $fuse_dir/our.apex >$fuse_dir/our.apex.list
174. <(classify $fuse_dir/ref.apex.list $fuse_dir/our.apex.list; /bin/true)
175
176# Now we have missing/extra/common holding respective file lists. Compare
177((${#missing[@]}==0)) || { printf "Missing files:"; printf " %s" ${missing[@]}; printf "\n"; }
178((${#extra[@]}==0)) || { printf "Extra files:"; printf " %s" ${extra[@]}; printf "\n"; }
179
180for f in "${common[@]}"; do
181    cmp -s $fuse_dir/{ref,our}.apex/$f && continue
182    # File differs, compare known file types intelligently
183    case $f in
184        AndroidManifest.xml)
185            echo $f differs:
186            diff \
187              <(aapt dump xmltree $fuse_dir/ref.apex AndroidManifest.xml) \
188              <(aapt dump xmltree $fuse_dir/our.apex AndroidManifest.xml) || /bin/true
189            ;;
190        apex_build_info.pb)
191            echo $f differs:
192            diff <(dump_buildinfo <$fuse_dir/ref.apex/$f) <(dump_buildinfo <$fuse_dir/our.apex/$f) || /bin/true
193            ;;
194        manifest.pb)
195            echo $f differs:
196            diff <(dump_apex_manifest <$fuse_dir/ref.apex/$f) <(dump_apex_manifest <$fuse_dir/our.apex/$f) || bin/true
197            ;;
198        apex_payload.img)
199            echo image $f differs, mounting it:
200            compare_images $fuse_dir/{ref,our}.apex/$f
201            ;;
202        META-INF/*)
203            # Ignore these. They are derived from the rest
204            # showing their difference does not help.
205            ;;
206        *) echo $f; diff $fuse_dir/{ref,our}.apex/$f || /bin/true
207    esac
208done
209