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/.
13 """ Define the transformations needed to make source + update == target
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.
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
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)
31 "defaults/pref/channel-prefs.js",
33 "channel_prefix": ["aurora", "beta", "release", "esr"],
35 "deletion": '//@line 6 "',
38 # updates from a beta to an RC build, the latter specifies the release channel
40 "defaults/pref/channel-prefs.js",
42 "channel_prefix": ["beta"],
45 'pref("app.update.channel", "release");\n',
46 'pref("app.update.channel", "beta");\n',
50 # updates from an RC to a beta build
52 "defaults/pref/channel-prefs.js",
54 "channel_prefix": ["beta"],
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.
68 "defaults/pref/channel-prefs.js",
70 "channel_prefix": ["aurora", "beta", "release", "esr"],
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"],
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.
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",
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 ()
126 logging
.warning("ignoring paths: {}".format(ignore_missing
))
128 left_diff
= obj1
- obj2
130 if left_diff
- set(ignore_missing
):
132 difference_found
= True
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
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
):
153 with
open(filename
, "rb", buffering
=0) as f
:
154 for b
in iter(lambda: f
.read(128 * 1024), b
""):
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(
167 ) != hash_file(target_file
):
168 logging
.info("Difference found in {}".format(filename
))
169 if filename
in IGNORE_FILES
:
171 "Ignoring difference in {} because it is listed in IGNORE_FILES".format(
178 "source": open(source_file
).readlines(),
179 "target": open(target_file
).readlines(),
185 if filename
in t
["files"]
186 and channel
.startswith(tuple(t
["channel_prefix"]))
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"]
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")
216 if file_contents
["source"] != file_contents
["target"]:
217 difference_found
= True
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")
237 "--verbose", "-v", action
="store_true", help="Enable verbose logging"
243 help="Ignore absence of <path> in the target",
246 args
= parser
.parse_args()
249 level
= logging
.DEBUG
250 logging
.basicConfig(level
=level
, format
="%(message)s", stream
=sys
.stdout
)
254 if not os
.path
.exists(source
) or not os
.path
.exists(target
):
255 logging
.error("Source and/or target directory doesn't exist")
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
273 # Use status of 2 since python will use 1 if there is an error running the script
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
280 logging
.info("No differences found")