update-verify-config-creator.py (30838B)
1 # This Source Code Form is subject to the terms of the Mozilla Public 2 # License, v. 2.0. If a copy of the MPL was not distributed with this 3 # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 5 import asyncio 6 import json 7 import math 8 import os 9 import pprint 10 import re 11 import sys 12 from collections import namedtuple 13 from urllib.parse import urljoin 14 15 import aiohttp 16 import hglib 17 from hglib.util import cmdbuilder 18 from looseversion import LooseVersion 19 from mozilla_version.gecko import GeckoVersion 20 from mozilla_version.version import VersionType 21 22 sys.path.insert(1, os.path.dirname(os.path.dirname(sys.path[0]))) 23 24 from mozharness.base.log import DEBUG, FATAL, INFO, WARNING 25 from mozharness.base.script import BaseScript, PostScriptRun, PreScriptRun 26 27 28 # ensure all versions are 3 part (i.e. 99.1.0) 29 # ensure all text (i.e. 'esr') is in the last part 30 class CompareVersion(LooseVersion): 31 version = "" 32 33 def __init__(self, versionMap): 34 parts = versionMap.split(".") 35 # assume version is 99.9.0, look for 99.0 36 if len(parts) == 2: 37 intre = re.compile("([0-9.]+)(.*)") 38 match = intre.match(parts[-1]) 39 if match: 40 parts[-1] = match.group(1) 41 parts.append("0%s" % match.group(2)) 42 else: 43 parts.append("0") 44 self.version = ".".join(parts) 45 LooseVersion(versionMap) 46 47 48 BuildInfo = namedtuple("BuildInfo", ["product", "version", "buildID"]) 49 50 51 def is_triangular(x): 52 """Check if a number is triangular (0, 1, 3, 6, 10, 15, ...) 53 see: https://en.wikipedia.org/wiki/Triangular_number#Triangular_roots_and_tests_for_triangular_numbers # noqa 54 55 >>> is_triangular(0) 56 True 57 >>> is_triangular(1) 58 True 59 >>> is_triangular(2) 60 False 61 >>> is_triangular(3) 62 True 63 >>> is_triangular(4) 64 False 65 >>> all(is_triangular(x) for x in [0, 1, 3, 6, 10, 15, 21, 28, 36, 45, 55, 66, 78, 91, 105]) 66 True 67 >>> all(not is_triangular(x) for x in [4, 5, 8, 9, 11, 17, 25, 29, 39, 44, 59, 61, 72, 98, 112]) 68 True 69 """ 70 # pylint --py3k W1619 71 n = (math.sqrt(8 * x + 1) - 1) / 2 72 return n == int(n) 73 74 75 class UpdateVerifyConfigCreator(BaseScript): 76 config_options = [ 77 [ 78 ["--product"], 79 { 80 "dest": "product", 81 "help": "Product being tested, as used in the update URL and filenames. Eg: firefox", # NOQA: E501 82 }, 83 ], 84 [ 85 ["--stage-product"], 86 { 87 "dest": "stage_product", 88 "help": "Product being tested, as used in stage directories and ship it" 89 "If not passed this is assumed to be the same as product.", 90 }, 91 ], 92 [ 93 ["--app-name"], 94 { 95 "dest": "app_name", 96 "help": "App name being tested. Eg: browser", 97 }, 98 ], 99 [ 100 ["--branch-prefix"], 101 { 102 "dest": "branch_prefix", 103 "help": "Prefix of release branch names. Eg: mozilla, comm", 104 }, 105 ], 106 [ 107 ["--channel"], 108 { 109 "dest": "channel", 110 "help": "Channel to run update verify against", 111 }, 112 ], 113 [ 114 ["--aus-server"], 115 { 116 "dest": "aus_server", 117 "default": "https://aus5.mozilla.org", 118 "help": "AUS server to run update verify against", 119 }, 120 ], 121 [ 122 ["--to-version"], 123 { 124 "dest": "to_version", 125 "help": "The version of the release being updated to. Eg: 59.0b5", 126 }, 127 ], 128 [ 129 ["--to-app-version"], 130 { 131 "dest": "to_app_version", 132 "help": "The in-app version of the release being updated to. Eg: 59.0", 133 }, 134 ], 135 [ 136 ["--to-display-version"], 137 { 138 "dest": "to_display_version", 139 "help": "The human-readable version of the release being updated to. Eg: 59.0 Beta 9", # NOQA: E501 140 }, 141 ], 142 [ 143 ["--to-build-number"], 144 { 145 "dest": "to_build_number", 146 "help": "The build number of the release being updated to", 147 }, 148 ], 149 [ 150 ["--to-buildid"], 151 { 152 "dest": "to_buildid", 153 "help": "The buildid of the release being updated to", 154 }, 155 ], 156 [ 157 ["--to-revision"], 158 { 159 "dest": "to_revision", 160 "help": "The revision that the release being updated to was built against", 161 }, 162 ], 163 [ 164 ["--partial-version"], 165 { 166 "dest": "partial_versions", 167 "default": [], 168 "action": "append", 169 "help": "A previous release version that is expected to receive a partial update. " 170 "Eg: 59.0b4. May be specified multiple times.", 171 }, 172 ], 173 [ 174 ["--last-watershed"], 175 { 176 "dest": "last_watershed", 177 "help": "The earliest version to include in the update verify config. Eg: 57.0b10", 178 }, 179 ], 180 [ 181 ["--include-version"], 182 { 183 "dest": "include_versions", 184 "default": [], 185 "action": "append", 186 "help": "Only include versions that match one of these regexes. " 187 "May be passed multiple times", 188 }, 189 ], 190 [ 191 ["--mar-channel-id-override"], 192 { 193 "dest": "mar_channel_id_options", 194 "default": [], 195 "action": "append", 196 "help": "A version regex and channel id string to override those versions with." 197 "Eg: ^\\d+\\.\\d+(\\.\\d+)?$,firefox-mozilla-beta,firefox-mozilla-release " 198 "will set accepted mar channel ids to 'firefox-mozilla-beta' and " 199 "'firefox-mozilla-release for x.y and x.y.z versions. " 200 "May be passed multiple times", 201 }, 202 ], 203 [ 204 ["--override-certs"], 205 { 206 "dest": "override_certs", 207 "default": None, 208 "help": "Certs to override the updater with prior to running update verify." 209 "If passed, should be one of: dep, nightly, release" 210 "If not passed, no certificate overriding will be configured", 211 }, 212 ], 213 [ 214 ["--platform"], 215 { 216 "dest": "platform", 217 "help": "The platform to generate the update verify config for, in FTP-style", 218 }, 219 ], 220 [ 221 ["--updater-platform"], 222 { 223 "dest": "updater_platform", 224 "help": "The platform to run the updater on, in FTP-style." 225 "If not specified, this is assumed to be the same as platform", 226 }, 227 ], 228 [ 229 ["--archive-prefix"], 230 { 231 "dest": "archive_prefix", 232 "help": "The server/path to pull the current release from. " 233 "Eg: https://archive.mozilla.org/pub", 234 }, 235 ], 236 [ 237 ["--previous-archive-prefix"], 238 { 239 "dest": "previous_archive_prefix", 240 "help": "The server/path to pull the previous releases from" 241 "If not specified, this is assumed to be the same as --archive-prefix", 242 }, 243 ], 244 [ 245 ["--repo-path"], 246 { 247 "dest": "repo_path", 248 "help": ( 249 "The repository (relative to the hg server root) that the current " 250 "release was built from Eg: releases/mozilla-beta" 251 ), 252 }, 253 ], 254 [ 255 ["--output-file"], 256 { 257 "dest": "output_file", 258 "help": "Where to write the update verify config to", 259 }, 260 ], 261 [ 262 ["--product-details-server"], 263 { 264 "dest": "product_details_server", 265 "default": "https://product-details.mozilla.org", 266 "help": "Product Details server to pull previous release info from. " 267 "Using anything other than the production server is likely to " 268 "cause issues with update verify.", 269 }, 270 ], 271 [ 272 ["--last-linux-bz2-version"], 273 { 274 "dest": "last_linux_bz2_version", 275 "help": "Last linux build version with bz2 compression.", 276 }, 277 ], 278 [ 279 ["--hg-server"], 280 { 281 "dest": "hg_server", 282 "default": "https://hg.mozilla.org", 283 "help": "Mercurial server to pull various previous and current version info from", 284 }, 285 ], 286 [ 287 ["--full-check-locale"], 288 { 289 "dest": "full_check_locales", 290 "default": ["de", "en-US", "ru"], 291 "action": "append", 292 "help": "A list of locales to generate full update verify checks for", 293 }, 294 ], 295 [ 296 ["--local-repo"], 297 { 298 "dest": "local_repo", 299 "help": "Path to local clone of the repository", 300 }, 301 ], 302 ] 303 304 def __init__(self): 305 BaseScript.__init__( 306 self, 307 config_options=self.config_options, 308 config={}, 309 all_actions=[ 310 "gather-info", 311 "create-config", 312 "write-config", 313 ], 314 default_actions=[ 315 "gather-info", 316 "create-config", 317 "write-config", 318 ], 319 ) 320 self.hgclient = None 321 322 @PreScriptRun 323 def _setup_hgclient(self): 324 if not self.config.get("local_repo"): 325 return 326 # Setup hgclient 327 self.hgclient = hglib.open(self.config["local_repo"]) 328 try: 329 self.hg_tags = set(t[0].decode("utf-8") for t in self.hgclient.tags()) 330 self.log(f"Loaded tags from local hg repo. {len(self.hg_tags)} tags found.") 331 except Exception as e: 332 self.log(f"Error loading tags from local hg repo: {e}") 333 self.hg_tags = set() 334 335 @PostScriptRun 336 def _close_hg_client(self): 337 if hasattr(self, "hgclient"): 338 self.hgclient.close() 339 self.log("Closed HG client.") 340 341 def _pre_config_lock(self, rw_config): 342 super()._pre_config_lock(rw_config) 343 344 if "updater_platform" not in self.config: 345 self.config["updater_platform"] = self.config["platform"] 346 if "stage_product" not in self.config: 347 self.config["stage_product"] = self.config["product"] 348 if "previous_archive_prefix" not in self.config: 349 self.config["previous_archive_prefix"] = self.config["archive_prefix"] 350 self.config["archive_prefix"].rstrip("/") 351 self.config["previous_archive_prefix"].rstrip("/") 352 self.config["mar_channel_id_overrides"] = {} 353 for override in self.config["mar_channel_id_options"]: 354 pattern, override_str = override.split(",", 1) 355 self.config["mar_channel_id_overrides"][pattern] = override_str 356 357 def _get_branch_url(self, branch_prefix, version): 358 version = GeckoVersion.parse(version) 359 branch = None 360 if version.version_type == VersionType.BETA: 361 branch = f"releases/{branch_prefix}-beta" 362 elif version.version_type == VersionType.ESR: 363 branch = f"releases/{branch_prefix}-esr{version.major_number}" 364 elif version.version_type == VersionType.RELEASE: 365 branch = f"releases/{branch_prefix}-release" 366 if not branch: 367 raise Exception("Cannot determine branch, cannot continue!") 368 369 return branch 370 371 async def _download_build_info( 372 self, semaphore, session, product, version, info_file_url 373 ): 374 """Async download and parse build info file for given url 375 376 Args: 377 semaphore: Semaphore object to control max async parallel channels 378 session: Http session handler 379 product: Product string 380 version: Version string 381 info_file_url: URL to desired buildid file 382 383 Returns: 384 BuildInfo Tuple (product, version, buildID) 385 """ 386 387 async def _get(): 388 async with session.get(info_file_url) as response: 389 if response.status < 400: 390 return response.status, await response.text() 391 return response.status, response.reason 392 393 RETRIES = 3 394 # Retry delay increase per attempt (5, 10, 15... seconds) 395 RETRY_DELAY_STEP = 5 396 async with semaphore: 397 attempt = 1 398 while attempt <= RETRIES: 399 self.log( 400 f"Retrieving buildid from info file: {info_file_url} - attempt: #{attempt}", 401 level=INFO, 402 ) 403 status, text = await _get() 404 if status < 400: 405 return BuildInfo(product, version, text.split("=")[1].strip()) 406 self.log( 407 f"Error retrieving buildid {info_file_url} - Status: {status} - Reason: {text}" 408 ) 409 if status == 404: 410 raise Exception(f"File not found on remote server: {info_file_url}") 411 attempt += 1 412 await asyncio.sleep(RETRY_DELAY_STEP * attempt) 413 raise Exception(f"Max number of retries reached for {info_file_url}") 414 415 def _async_download_build_ids(self, filelist): 416 """Download all build_info asynchronously, then process once everything is downloaded 417 418 Args: 419 filelist: List of tuples (product, version, info_file_url) 420 Returns: 421 List of BuildInfo tuples (product, version, buildID) 422 """ 423 CONCURRENCY = 15 424 # TODO: We need to rewrite mozharness.BaseScript to be async before we can properly handle async coroutines. 425 loop = asyncio.get_event_loop() 426 427 async def _run_semaphore(): 428 async with aiohttp.ClientSession() as session: 429 self.log( 430 f"Starting async download. Semaphore with {CONCURRENCY} concurrencies." 431 ) 432 semaphore = asyncio.Semaphore(CONCURRENCY) 433 tasks = [ 434 self._download_build_info(semaphore, session, *info) 435 for info in filelist 436 ] 437 return await asyncio.gather(*tasks) 438 439 return loop.run_until_complete(_run_semaphore()) 440 441 def _get_update_paths(self): 442 from mozrelease.l10n import getPlatformLocales 443 from mozrelease.paths import getCandidatesDir 444 from mozrelease.platforms import ftp2infoFile 445 from mozrelease.versions import MozillaVersion 446 447 self.update_paths = {} 448 449 ret = self._retry_download( 450 "{}/1.0/{}.json".format( 451 self.config["product_details_server"], 452 self.config["stage_product"], 453 ), 454 "WARNING", 455 ) 456 releases = json.load(ret)["releases"] 457 info_file_urls = [] 458 # Generate list of info_file_urls to be downloaded 459 for release_name, release_info in reversed( 460 sorted(releases.items(), key=lambda x: MozillaVersion(x[1]["version"])) 461 ): 462 # we need to use releases_name instead of release_info since esr 463 # string is included in the name. later we rely on this. 464 product, version = release_name.split("-", 1) 465 466 # Exclude any releases that don't match one of our include version 467 # regexes. This is generally to avoid including versions from other 468 # channels. Eg: including betas when testing releases 469 for v in self.config["include_versions"]: 470 if re.match(v, version): 471 break 472 else: 473 self.log( 474 "Skipping release whose version doesn't match any " 475 "include_version pattern: %s" % release_name, 476 level=INFO, 477 ) 478 continue 479 480 # We also have to trim out previous releases that aren't in the same 481 # product line, too old, etc. 482 if self.config["stage_product"] != product: 483 self.log( 484 "Skipping release that doesn't match product name: %s" 485 % release_name, 486 level=INFO, 487 ) 488 continue 489 if MozillaVersion(version) < MozillaVersion(self.config["last_watershed"]): 490 self.log( 491 "Skipping release that's behind the last watershed: %s" 492 % release_name, 493 level=INFO, 494 ) 495 continue 496 if version == self.config["to_version"]: 497 self.log( 498 "Skipping release that is the same as to version: %s" 499 % release_name, 500 level=INFO, 501 ) 502 continue 503 if MozillaVersion(version) > MozillaVersion(self.config["to_version"]): 504 self.log( 505 "Skipping release that's newer than to version: %s" % release_name, 506 level=INFO, 507 ) 508 continue 509 510 # This is a crappy place to get buildids from, but we don't have a better one. 511 # This will start to fail if old info files are deleted. 512 info_file_source = "{}{}/{}_info.txt".format( 513 self.config["previous_archive_prefix"], 514 getCandidatesDir( 515 self.config["stage_product"], 516 version, 517 release_info["build_number"], 518 ), 519 ftp2infoFile(self.config["platform"]), 520 ) 521 info_file_urls.append((product, version, info_file_source)) 522 523 build_info_list = self._async_download_build_ids(info_file_urls) 524 525 for build in build_info_list: 526 if build.version in self.update_paths: 527 raise Exception( 528 "Found duplicate release for version: %s", build.version 529 ) 530 531 shipped_locales, app_version = self._get_files_from_repo_tag( 532 build.product, 533 build.version, 534 f"{self.config['app_name']}/locales/shipped-locales", 535 f"{self.config['app_name']}/config/version.txt", 536 ) 537 self.log(f"Adding {build.version} to update paths", level=INFO) 538 self.update_paths[build.version] = { 539 "appVersion": app_version, 540 "locales": getPlatformLocales(shipped_locales, self.config["platform"]), 541 "buildID": build.buildID, 542 } 543 for pattern, mar_channel_ids in self.config[ 544 "mar_channel_id_overrides" 545 ].items(): 546 if re.match(pattern, build.version): 547 self.update_paths[build.version]["marChannelIds"] = mar_channel_ids 548 549 def _get_file_from_repo(self, rev, branch, path): 550 if self.config.get("local_repo"): 551 try: 552 return self._get_files_from_local_repo(rev, path)[0] 553 except Exception: 554 self.log( 555 "Unable to get file from local repo, trying from remote instead." 556 ) 557 return self._get_files_from_remote_repo(rev, branch, path)[0] 558 559 def _get_files_from_repo_tag(self, product, version, *paths): 560 tag = "{}_{}_RELEASE".format(product.upper(), version.replace(".", "_")) 561 if self.config.get("local_repo") and tag in self.hg_tags: 562 return self._get_files_from_local_repo(tag, *paths) 563 branch = self._get_branch_url(self.config["branch_prefix"], version) 564 return self._get_files_from_remote_repo(tag, branch, *paths) 565 566 def _get_files_from_local_repo(self, rev, *paths): 567 """Retrieve multiple files from the local repo at a given revision""" 568 # Given how slow hg is to retrieve files at specific revisions, 569 # the only performance improvement we can get is to cat multiple 570 # files and use a \\0 block separator. It's ugly, but it works. 571 args = cmdbuilder( 572 b"cat", 573 *[bytes(p, "utf-8") for p in paths], 574 cwd=self.config["local_repo"].encode("utf-8"), 575 r=bytes(rev, "utf-8"), 576 T=b"{path}\\0{data}\\0", 577 ) 578 try: 579 raw = self.hgclient.rawcommand(args).strip().decode("utf-8") 580 except Exception as e: 581 self.log("Error retrieving file from local repository.") 582 raise e 583 584 # The separator is added after every file data - so we need to remove the last one 585 # Note that \\0 becomes \x00 (null) on the output side 586 chunks = raw.split("\x00")[:-1] 587 # The first line is the file path, so we map the path to contents 588 path_contents = {} 589 while chunks: 590 filename = chunks.pop(0) 591 data = chunks.pop(0).strip() 592 path_contents[filename] = data 593 594 # Result should be the same order as requested 595 result = [] 596 for path in paths: 597 if path not in path_contents: 598 raise Exception( 599 f"_get_files_from_local_repo: Could not find {path} in revision {rev}" 600 ) 601 result.append(path_contents[path]) 602 return result 603 604 def _get_files_from_remote_repo(self, rev, branch, *paths): 605 files = [] 606 for path in paths: 607 hg_url = urljoin( 608 self.config["hg_server"], f"{branch}/raw-file/{rev}/{path}" 609 ) 610 # we're going to waste time retrying on 404s here...meh 611 # at least we can lower sleep time to minimize that 612 ret = self._retry_download( 613 hg_url, "WARNING", retry_config={"sleeptime": 5, "max_sleeptime": 5} 614 ) 615 # yep...errors are not raised! they're indicated by a `None` 616 if ret is None: 617 self.log("couldn't fetch file from hg; trying github") 618 # this won't work for try most likely; that's okay, it's a short term hack! 619 # possible problems: 620 # - we get a non tag rev 621 # - we get rate limited 622 git_url = f"https://raw.githubusercontent.com/mozilla-firefox/firefox/refs/tags/{rev}/{path}" 623 ret = self._retry_download(git_url, "WARNING") 624 625 files.append(ret.read().strip().decode("utf-8")) 626 return files 627 628 def gather_info(self): 629 from mozilla_version.gecko import GeckoVersion 630 631 self._get_update_paths() 632 if self.update_paths: 633 self.log("Found update paths:", level=DEBUG) 634 self.log(pprint.pformat(self.update_paths), level=DEBUG) 635 elif GeckoVersion.parse(self.config["to_version"]) <= GeckoVersion.parse( 636 self.config["last_watershed"] 637 ): 638 self.log( 639 "Didn't find any update paths, but to_version {} is before the last_" 640 "watershed {}, generating empty config".format( 641 self.config["to_version"], 642 self.config["last_watershed"], 643 ), 644 level=WARNING, 645 ) 646 else: 647 self.log("Didn't find any update paths, cannot continue", level=FATAL) 648 649 def create_config(self): 650 from mozrelease.l10n import getPlatformLocales 651 from mozrelease.paths import ( 652 getCandidatesDir, 653 getReleaseInstallerPath, 654 getReleasesDir, 655 ) 656 from mozrelease.platforms import ftp2updatePlatforms 657 from mozrelease.update_verify import UpdateVerifyConfig 658 from mozrelease.versions import getPrettyVersion 659 660 candidates_dir = getCandidatesDir( 661 self.config["stage_product"], 662 self.config["to_version"], 663 self.config["to_build_number"], 664 ) 665 to_ = getReleaseInstallerPath( 666 self.config["product"], 667 self.config["product"].title(), 668 self.config["to_version"], 669 self.config["platform"], 670 locale="%locale%", 671 last_linux_bz2_version=self.config.get("last_linux_bz2_version"), 672 ) 673 to_path = f"{candidates_dir}/{to_}" 674 675 to_display_version = self.config.get("to_display_version") 676 if not to_display_version: 677 to_display_version = getPrettyVersion(self.config["to_version"]) 678 679 self.update_verify_config = UpdateVerifyConfig( 680 product=self.config["product"].title(), 681 channel=self.config["channel"], 682 aus_server=self.config["aus_server"], 683 to=to_path, 684 to_build_id=self.config["to_buildid"], 685 to_app_version=self.config["to_app_version"], 686 to_display_version=to_display_version, 687 override_certs=self.config.get("override_certs"), 688 ) 689 690 to_shipped_locales = self._get_file_from_repo( 691 self.config["to_revision"], 692 self.config["repo_path"], 693 "{}/locales/shipped-locales".format(self.config["app_name"]), 694 ) 695 to_locales = set( 696 getPlatformLocales(to_shipped_locales, self.config["platform"]) 697 ) 698 699 completes_only_index = 0 700 for fromVersion in reversed(sorted(self.update_paths, key=CompareVersion)): 701 from_ = self.update_paths[fromVersion] 702 locales = sorted(list(set(from_["locales"]).intersection(to_locales))) 703 appVersion = from_["appVersion"] 704 build_id = from_["buildID"] 705 mar_channel_IDs = from_.get("marChannelIds") 706 707 # Use new build targets for Windows, but only on compatible 708 # versions (42+). See bug 1185456 for additional context. 709 if self.config["platform"] not in ("win32", "win64") or LooseVersion( 710 fromVersion 711 ) < LooseVersion("42.0"): 712 update_platform = ftp2updatePlatforms(self.config["platform"])[0] 713 else: 714 update_platform = ftp2updatePlatforms(self.config["platform"])[1] 715 716 release_dir = getReleasesDir(self.config["stage_product"], fromVersion) 717 path_ = getReleaseInstallerPath( 718 self.config["product"], 719 self.config["product"].title(), 720 fromVersion, 721 self.config["platform"], 722 locale="%locale%", 723 last_linux_bz2_version=self.config.get("last_linux_bz2_version"), 724 ) 725 from_path = f"{release_dir}/{path_}" 726 727 updater_package = "{}/{}".format( 728 release_dir, 729 getReleaseInstallerPath( 730 self.config["product"], 731 self.config["product"].title(), 732 fromVersion, 733 self.config["updater_platform"], 734 locale="%locale%", 735 last_linux_bz2_version=self.config.get("last_linux_bz2_version"), 736 ), 737 ) 738 739 # Exclude locales being full checked 740 quick_check_locales = [ 741 l for l in locales if l not in self.config["full_check_locales"] 742 ] 743 # Get the intersection of from and to full_check_locales 744 this_full_check_locales = [ 745 l for l in self.config["full_check_locales"] if l in locales 746 ] 747 748 if fromVersion in self.config["partial_versions"]: 749 self.info( 750 "Generating configs for partial update checks for %s" % fromVersion 751 ) 752 self.update_verify_config.addRelease( 753 release=appVersion, 754 build_id=build_id, 755 locales=locales, 756 patch_types=["complete", "partial"], 757 from_path=from_path, 758 ftp_server_from=self.config["previous_archive_prefix"], 759 ftp_server_to=self.config["archive_prefix"], 760 mar_channel_IDs=mar_channel_IDs, 761 platform=update_platform, 762 updater_package=updater_package, 763 ) 764 else: 765 if this_full_check_locales and is_triangular(completes_only_index): 766 self.info("Generating full check configs for %s" % fromVersion) 767 self.update_verify_config.addRelease( 768 release=appVersion, 769 build_id=build_id, 770 locales=this_full_check_locales, 771 from_path=from_path, 772 ftp_server_from=self.config["previous_archive_prefix"], 773 ftp_server_to=self.config["archive_prefix"], 774 mar_channel_IDs=mar_channel_IDs, 775 platform=update_platform, 776 updater_package=updater_package, 777 ) 778 # Quick test for other locales, no download 779 if len(quick_check_locales) > 0: 780 self.info("Generating quick check configs for %s" % fromVersion) 781 if not is_triangular(completes_only_index): 782 # Assuming we skipped full check locales, using all locales 783 _locales = locales 784 else: 785 # Excluding full check locales from the quick check 786 _locales = quick_check_locales 787 self.update_verify_config.addRelease( 788 release=appVersion, 789 build_id=build_id, 790 locales=_locales, 791 platform=update_platform, 792 ) 793 completes_only_index += 1 794 795 def write_config(self): 796 # Needs to be opened in "bytes" mode because we perform relative seeks on it 797 with open(self.config["output_file"], "wb+") as fh: 798 self.update_verify_config.write(fh) 799 800 801 if __name__ == "__main__": 802 UpdateVerifyConfigCreator().run_and_exit()