Backed out 2 changesets (bug 1943998) for causing wd failures @ phases.py CLOSED...
[gecko.git] / tools / update-verify / release / compare-directories.py
blob714f02a66d7cd2e247161c467a09bfa267cfb797
1 #! /usr/bin/env python3
2 # This Source Code Form is subject to the terms of the Mozilla Public
3 # License, v. 2.0. If a copy of the MPL was not distributed with this file,
4 # You can obtain one at http://mozilla.org/MPL/2.0/.
6 import argparse
7 import difflib
8 import hashlib
9 import logging
10 import os
11 import sys
13 """ Define the transformations needed to make source + update == target
15 Required:
16 The files list describes the files which a transform may be used on.
17 The 'side' is one of ('source', 'target') and defines where each transform is applied
18 The 'channel_prefix' list controls which channels a transform may be used for, where a value of
19 'beta' means all of beta, beta-localtest, beta-cdntest, etc.
21 One or more:
22 A 'deletion' specifies a start of line to match on, removing the whole line
23 A 'substitution' is a list of full string to match and its replacement
24 """
25 TRANSFORMS = [
26 # channel-prefs.js
28 # preprocessor comments, eg //@line 6 "/builds/worker/workspace/...
29 # this can be removed once each channel has a watershed above 59.0b2 (from bug 1431342)
30 "files": [
31 "defaults/pref/channel-prefs.js",
33 "channel_prefix": ["aurora", "beta", "release", "esr"],
34 "side": "source",
35 "deletion": '//@line 6 "',
38 # updates from a beta to an RC build, the latter specifies the release channel
39 "files": [
40 "defaults/pref/channel-prefs.js",
42 "channel_prefix": ["beta"],
43 "side": "target",
44 "substitution": [
45 'pref("app.update.channel", "release");\n',
46 'pref("app.update.channel", "beta");\n',
50 # updates from an RC to a beta build
51 "files": [
52 "defaults/pref/channel-prefs.js",
54 "channel_prefix": ["beta"],
55 "side": "source",
56 "substitution": [
57 'pref("app.update.channel", "release");\n',
58 'pref("app.update.channel", "beta");\n',
62 # Warning comments from bug 1576546
63 # When updating from a pre-70.0 build to 70.0+ this removes the new comments in
64 # the target side. In the 70.0+ --> 70.0+ case with a RC we won't need this, and
65 # the channel munging above will make channel-prefs.js identical, allowing the code
66 # to break before applying this transform.
67 "files": [
68 "defaults/pref/channel-prefs.js",
70 "channel_prefix": ["aurora", "beta", "release", "esr"],
71 "side": "target",
72 "deletion": "//",
74 # update-settings.ini
76 # updates from a beta to an RC build, the latter specifies the release channel
77 # on mac, we actually have both files. The second location is the real
78 # one but we copy to the first to run the linux64 updater
79 "files": ["update-settings.ini"],
80 "channel_prefix": ["beta"],
81 "side": "target",
82 "substitution": [
83 "ACCEPTED_MAR_CHANNEL_IDS=firefox-mozilla-release\n",
84 "ACCEPTED_MAR_CHANNEL_IDS=firefox-mozilla-beta,firefox-mozilla-release\n",
89 # Files that are expected to be different, but cannot be transformed to get a useful diff.
90 # This should generally only be used for files that have unpredictable contents, eg:
91 # things that are signed but not updated.
92 IGNORE_FILES = (
93 "Contents/MacOS/updater.app/Contents/Frameworks/UpdateSettings.framework/Resources/Info.plist",
94 "Contents/MacOS/updater.app/Contents/Frameworks/UpdateSettings.framework/_CodeSignature/CodeResources",
95 "Contents/MacOS/updater.app/Contents/Frameworks/UpdateSettings.framework/UpdateSettings",
96 "Contents/Frameworks/ChannelPrefs.framework/Resources/Info.plist",
97 "Contents/Frameworks/ChannelPrefs.framework/_CodeSignature/CodeResources",
98 "Contents/Frameworks/ChannelPrefs.framework/ChannelPrefs",
102 def walk_dir(path):
103 all_files = []
104 all_dirs = []
106 for root, dirs, files in os.walk(path):
107 all_dirs.extend([os.path.join(root, d) for d in dirs])
108 all_files.extend([os.path.join(root, f) for f in files])
110 # trim off directory prefix for easier comparison
111 all_dirs = [d[len(path) + 1 :] for d in all_dirs]
112 all_files = [f[len(path) + 1 :] for f in all_files]
114 return all_dirs, all_files
117 def compare_listings(
118 source_list, target_list, label, source_dir, target_dir, ignore_missing=None
120 obj1 = set(source_list)
121 obj2 = set(target_list)
122 difference_found = False
123 ignore_missing = ignore_missing or ()
125 if ignore_missing:
126 logging.warning("ignoring paths: {}".format(ignore_missing))
128 left_diff = obj1 - obj2
129 if left_diff:
130 if left_diff - set(ignore_missing):
131 _log = logging.error
132 difference_found = True
133 else:
134 _log = logging.warning
135 _log("Ignoring missing files due to ignore_missing")
137 _log("{} only in {}:".format(label, source_dir))
138 for d in sorted(left_diff):
139 _log(" {}".format(d))
141 right_diff = obj2 - obj1
142 if right_diff:
143 logging.error("{} only in {}:".format(label, target_dir))
144 for d in sorted(right_diff):
145 logging.error(" {}".format(d))
146 difference_found = True
148 return difference_found
151 def hash_file(filename):
152 h = hashlib.sha256()
153 with open(filename, "rb", buffering=0) as f:
154 for b in iter(lambda: f.read(128 * 1024), b""):
155 h.update(b)
156 return h.hexdigest()
159 def compare_common_files(files, channel, source_dir, target_dir):
160 difference_found = False
161 for filename in files:
162 source_file = os.path.join(source_dir, filename)
163 target_file = os.path.join(target_dir, filename)
165 if os.stat(source_file).st_size != os.stat(target_file).st_size or hash_file(
166 source_file
167 ) != hash_file(target_file):
168 logging.info("Difference found in {}".format(filename))
169 if filename in IGNORE_FILES:
170 logging.info(
171 "Ignoring difference in {} because it is listed in IGNORE_FILES".format(
172 filename
175 continue
177 file_contents = {
178 "source": open(source_file).readlines(),
179 "target": open(target_file).readlines(),
182 transforms = [
184 for t in TRANSFORMS
185 if filename in t["files"]
186 and channel.startswith(tuple(t["channel_prefix"]))
188 logging.debug(
189 "Got {} transform(s) to consider for {}".format(
190 len(transforms), filename
193 for transform in transforms:
194 side = transform["side"]
196 if "deletion" in transform:
197 d = transform["deletion"]
198 logging.debug(
199 "Trying deleting lines starting {} from {}".format(d, side)
201 file_contents[side] = [
202 l for l in file_contents[side] if not l.startswith(d)
205 if "substitution" in transform:
206 r = transform["substitution"]
207 logging.debug("Trying replacement for {} in {}".format(r, side))
208 file_contents[side] = [
209 l.replace(r[0], r[1]) for l in file_contents[side]
212 if file_contents["source"] == file_contents["target"]:
213 logging.info("Transforms removed all differences")
214 break
216 if file_contents["source"] != file_contents["target"]:
217 difference_found = True
218 logging.error(
219 "{} still differs after transforms, residual diff:".format(filename)
221 for l in difflib.unified_diff(
222 file_contents["source"], file_contents["target"]
224 logging.error(l.rstrip())
226 return difference_found
229 if __name__ == "__main__":
230 parser = argparse.ArgumentParser(
231 "Compare two directories recursively, with transformations for expected diffs"
233 parser.add_argument("source", help="Directory containing updated Firefox")
234 parser.add_argument("target", help="Directory containing expected Firefox")
235 parser.add_argument("channel", help="Update channel used")
236 parser.add_argument(
237 "--verbose", "-v", action="store_true", help="Enable verbose logging"
239 parser.add_argument(
240 "--ignore-missing",
241 action="append",
242 metavar="<path>",
243 help="Ignore absence of <path> in the target",
246 args = parser.parse_args()
247 level = logging.INFO
248 if args.verbose:
249 level = logging.DEBUG
250 logging.basicConfig(level=level, format="%(message)s", stream=sys.stdout)
252 source = args.source
253 target = args.target
254 if not os.path.exists(source) or not os.path.exists(target):
255 logging.error("Source and/or target directory doesn't exist")
256 sys.exit(3)
258 logging.info("Comparing {} with {}...".format(source, target))
259 source_dirs, source_files = walk_dir(source)
260 target_dirs, target_files = walk_dir(target)
262 dir_list_diff = compare_listings(
263 source_dirs, target_dirs, "Directories", source, target
265 file_list_diff = compare_listings(
266 source_files, target_files, "Files", source, target, args.ignore_missing
268 file_diff = compare_common_files(
269 set(source_files) & set(target_files), args.channel, source, target
272 if file_diff:
273 # Use status of 2 since python will use 1 if there is an error running the script
274 sys.exit(2)
275 elif dir_list_diff or file_list_diff:
276 # this has traditionally been a WARN, but we don't have files on one
277 # side anymore so lets FAIL
278 sys.exit(2)
279 else:
280 logging.info("No differences found")