1# -*- coding: utf-8 -*- 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 4# file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 6from __future__ import absolute_import, print_function 7 8import os 9import re 10from six import string_types 11 12from .chunking import getChunk 13 14 15class UpdateVerifyError(Exception): 16 pass 17 18 19class UpdateVerifyConfig(object): 20 comment_regex = re.compile("^#") 21 key_write_order = ( 22 "release", 23 "product", 24 "platform", 25 "build_id", 26 "locales", 27 "channel", 28 "patch_types", 29 "from", 30 "aus_server", 31 "ftp_server_from", 32 "ftp_server_to", 33 "to", 34 "mar_channel_IDs", 35 "override_certs", 36 "to_build_id", 37 "to_display_version", 38 "to_app_version", 39 "updater_package", 40 ) 41 global_keys = ( 42 "product", 43 "channel", 44 "aus_server", 45 "to", 46 "to_build_id", 47 "to_display_version", 48 "to_app_version", 49 "override_certs", 50 ) 51 release_keys = ( 52 "release", 53 "build_id", 54 "locales", 55 "patch_types", 56 "from", 57 "ftp_server_from", 58 "ftp_server_to", 59 "mar_channel_IDs", 60 "platform", 61 "updater_package", 62 ) 63 first_only_keys = ( 64 "from", 65 "aus_server", 66 "to", 67 "to_build_id", 68 "to_display_version", 69 "to_app_version", 70 "override_certs", 71 ) 72 compare_attrs = global_keys + ("releases",) 73 74 def __init__( 75 self, 76 product=None, 77 channel=None, 78 aus_server=None, 79 to=None, 80 to_build_id=None, 81 to_display_version=None, 82 to_app_version=None, 83 override_certs=None, 84 ): 85 self.product = product 86 self.channel = channel 87 self.aus_server = aus_server 88 self.to = to 89 self.to_build_id = to_build_id 90 self.to_display_version = to_display_version 91 self.to_app_version = to_app_version 92 self.override_certs = override_certs 93 self.releases = [] 94 95 def __eq__(self, other): 96 self_list = [getattr(self, attr) for attr in self.compare_attrs] 97 other_list = [getattr(other, attr) for attr in self.compare_attrs] 98 return self_list == other_list 99 100 def __ne__(self, other): 101 return not self.__eq__(other) 102 103 def _parseLine(self, line): 104 entry = {} 105 items = re.findall(r"\w+=[\"'][^\"']*[\"']", line) 106 for i in items: 107 m = re.search(r"(?P<key>\w+)=[\"'](?P<value>.+)[\"']", i).groupdict() 108 if m["key"] not in self.global_keys and m["key"] not in self.release_keys: 109 raise UpdateVerifyError( 110 "Unknown key '%s' found on line:\n%s" % (m["key"], line) 111 ) 112 if m["key"] in entry: 113 raise UpdateVerifyError( 114 "Multiple values found for key '%s' on line:\n%s" % (m["key"], line) 115 ) 116 entry[m["key"]] = m["value"] 117 if not entry: 118 raise UpdateVerifyError("No parseable data in line '%s'" % line) 119 return entry 120 121 def _addEntry(self, entry, first): 122 releaseKeys = {} 123 for k, v in entry.items(): 124 if k in self.global_keys: 125 setattr(self, k, entry[k]) 126 elif k in self.release_keys: 127 # "from" is reserved in Python 128 if k == "from": 129 releaseKeys["from_path"] = v 130 else: 131 releaseKeys[k] = v 132 self.addRelease(**releaseKeys) 133 134 def read(self, config): 135 f = open(config) 136 # Only the first non-comment line of an update verify config should 137 # have a "from" and"ausServer". Ignore any subsequent lines with them. 138 first = True 139 for line in f.readlines(): 140 # Skip comment lines 141 if self.comment_regex.search(line): 142 continue 143 self._addEntry(self._parseLine(line), first) 144 first = False 145 146 def write(self, fh): 147 first = True 148 for releaseInfo in self.releases: 149 for key in self.key_write_order: 150 if key in self.global_keys and ( 151 first or key not in self.first_only_keys 152 ): 153 value = getattr(self, key) 154 elif key in self.release_keys: 155 value = releaseInfo[key] 156 else: 157 value = None 158 if value is not None: 159 fh.write(key.encode("utf-8")) 160 fh.write(b"=") 161 if isinstance(value, (list, tuple)): 162 fh.write(('"%s" ' % " ".join(value)).encode("utf-8")) 163 else: 164 fh.write(('"%s" ' % value).encode("utf-8")) 165 # Rewind one character to avoid having a trailing space 166 fh.seek(-1, os.SEEK_CUR) 167 fh.write(b"\n") 168 first = False 169 170 def addRelease( 171 self, 172 release=None, 173 build_id=None, 174 locales=[], 175 patch_types=["complete"], 176 from_path=None, 177 ftp_server_from=None, 178 ftp_server_to=None, 179 mar_channel_IDs=None, 180 platform=None, 181 updater_package=None, 182 ): 183 """Locales and patch_types can be passed as either a string or a list. 184 If a string is passed, they will be converted to a list for internal 185 storage""" 186 if self.getRelease(build_id, from_path): 187 raise UpdateVerifyError( 188 "Couldn't add release identified by build_id '%s' and from_path '%s': " 189 "already exists in config" % (build_id, from_path) 190 ) 191 if isinstance(locales, string_types): 192 locales = sorted(list(locales.split())) 193 if isinstance(patch_types, string_types): 194 patch_types = list(patch_types.split()) 195 self.releases.append( 196 { 197 "release": release, 198 "build_id": build_id, 199 "locales": locales, 200 "patch_types": patch_types, 201 "from": from_path, 202 "ftp_server_from": ftp_server_from, 203 "ftp_server_to": ftp_server_to, 204 "mar_channel_IDs": mar_channel_IDs, 205 "platform": platform, 206 "updater_package": updater_package, 207 } 208 ) 209 210 def addLocaleToRelease(self, build_id, locale, from_path=None): 211 r = self.getRelease(build_id, from_path) 212 if not r: 213 raise UpdateVerifyError( 214 "Couldn't add '%s' to release identified by build_id '%s' and from_path '%s': " 215 "'%s' doesn't exist in this config." 216 % (locale, build_id, from_path, build_id) 217 ) 218 r["locales"].append(locale) 219 r["locales"] = sorted(r["locales"]) 220 221 def getRelease(self, build_id, from_path): 222 for r in self.releases: 223 if r["build_id"] == build_id and r["from"] == from_path: 224 return r 225 return {} 226 227 def getFullReleaseTests(self): 228 return [r for r in self.releases if r["from"] is not None] 229 230 def getQuickReleaseTests(self): 231 return [r for r in self.releases if r["from"] is None] 232 233 def getChunk(self, chunks, thisChunk): 234 fullTests = [] 235 quickTests = [] 236 for test in self.getFullReleaseTests(): 237 for locale in test["locales"]: 238 fullTests.append([test["build_id"], locale, test["from"]]) 239 for test in self.getQuickReleaseTests(): 240 for locale in test["locales"]: 241 quickTests.append([test["build_id"], locale, test["from"]]) 242 allTests = getChunk(fullTests, chunks, thisChunk) 243 allTests.extend(getChunk(quickTests, chunks, thisChunk)) 244 245 newConfig = UpdateVerifyConfig( 246 self.product, 247 self.channel, 248 self.aus_server, 249 self.to, 250 self.to_build_id, 251 self.to_display_version, 252 self.to_app_version, 253 self.override_certs, 254 ) 255 for t in allTests: 256 build_id, locale, from_path = t 257 if from_path == "None": 258 from_path = None 259 r = self.getRelease(build_id, from_path) 260 try: 261 newConfig.addRelease( 262 r["release"], 263 build_id, 264 locales=[], 265 ftp_server_from=r["ftp_server_from"], 266 ftp_server_to=r["ftp_server_to"], 267 patch_types=r["patch_types"], 268 from_path=from_path, 269 mar_channel_IDs=r["mar_channel_IDs"], 270 platform=r["platform"], 271 updater_package=r["updater_package"], 272 ) 273 except UpdateVerifyError: 274 pass 275 newConfig.addLocaleToRelease(build_id, locale, from_path) 276 return newConfig 277