| OLD | NEW |
| (Empty) |
| 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 os | |
| 6 import sys | |
| 7 import re | |
| 8 import hashlib | |
| 9 import base64 | |
| 10 import urllib | |
| 11 import json | |
| 12 import io | |
| 13 from ConfigParser import SafeConfigParser | |
| 14 from StringIO import StringIO | |
| 15 import xml.dom.minidom as minidom | |
| 16 import buildtools.localeTools as localeTools | |
| 17 | |
| 18 import packager | |
| 19 from packager import readMetadata, getMetadataPath, getDefaultFileName, getBuild
Version, getTemplate, Files | |
| 20 | |
| 21 KNOWN_APPS = { | |
| 22 'conkeror': '{a79fe89b-6662-4ff4-8e88-09950ad4dfde}', | |
| 23 'emusic': 'dlm@emusic.com', | |
| 24 'fennec': '{a23983c0-fd0e-11dc-95ff-0800200c9a66}', | |
| 25 'fennec2': '{aa3c5121-dab2-40e2-81ca-7ea25febc110}', | |
| 26 'firefox': '{ec8030f7-c20a-464f-9b0e-13a3a9e97384}', | |
| 27 'midbrowser': '{aa5ca914-c309-495d-91cf-3141bbb04115}', | |
| 28 'prism': 'prism@developer.mozilla.org', | |
| 29 'seamonkey': '{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}', | |
| 30 'songbird': 'songbird@songbirdnest.com', | |
| 31 'thunderbird': '{3550f703-e582-4d05-9a08-453d09bdfdc6}', | |
| 32 'toolkit': 'toolkit@mozilla.org', | |
| 33 'adblockbrowser': '{55aba3ac-94d3-41a8-9e25-5c21fe874539}', | |
| 34 } | |
| 35 | |
| 36 defaultLocale = 'en-US' | |
| 37 | |
| 38 | |
| 39 def getChromeDir(baseDir): | |
| 40 return os.path.join(baseDir, 'chrome') | |
| 41 | |
| 42 | |
| 43 def getLocalesDir(baseDir): | |
| 44 return os.path.join(getChromeDir(baseDir), 'locale') | |
| 45 | |
| 46 | |
| 47 def getChromeSubdirs(baseDir, locales): | |
| 48 result = {} | |
| 49 chromeDir = getChromeDir(baseDir) | |
| 50 for subdir in ('content', 'skin'): | |
| 51 result[subdir] = os.path.join(chromeDir, subdir) | |
| 52 for locale in locales: | |
| 53 result['locale/%s' % locale] = os.path.join(chromeDir, 'locale', locale) | |
| 54 return result | |
| 55 | |
| 56 | |
| 57 def getPackageFiles(params): | |
| 58 result = { | |
| 59 'chrome', 'components', 'modules', 'lib', 'resources', 'webextension', | |
| 60 'chrome.manifest', 'icon.png', 'icon64.png' | |
| 61 } | |
| 62 | |
| 63 baseDir = params['baseDir'] | |
| 64 for file in os.listdir(baseDir): | |
| 65 if file.endswith('.js') or file.endswith('.xml'): | |
| 66 result.add(file) | |
| 67 return result | |
| 68 | |
| 69 | |
| 70 def getIgnoredFiles(params): | |
| 71 return {'.incomplete', 'meta.properties'} | |
| 72 | |
| 73 | |
| 74 def archive_path(path, baseDir): | |
| 75 return '/'.join(os.path.split(os.path.relpath(path, baseDir))) | |
| 76 | |
| 77 | |
| 78 def isValidLocale(localesDir, dir, includeIncomplete=False): | |
| 79 if re.search(r'[^\w\-]', dir): | |
| 80 return False | |
| 81 curLocaleDir = os.path.join(localesDir, dir) | |
| 82 if not os.path.isdir(curLocaleDir): | |
| 83 return False | |
| 84 if len(os.listdir(curLocaleDir)) == 0: | |
| 85 return False | |
| 86 if not includeIncomplete and os.path.exists(os.path.join(localesDir, dir, '.
incomplete')): | |
| 87 return False | |
| 88 return True | |
| 89 | |
| 90 | |
| 91 def getLocales(baseDir, includeIncomplete=False): | |
| 92 global defaultLocale | |
| 93 localesDir = getLocalesDir(baseDir) | |
| 94 locales = filter(lambda dir: isValidLocale(localesDir, dir, includeIncomplet
e), os.listdir(localesDir)) | |
| 95 locales.sort(key=lambda x: '!' if x == defaultLocale else x) | |
| 96 return locales | |
| 97 | |
| 98 | |
| 99 def processFile(path, data, params): | |
| 100 if path.endswith('.manifest') and data.find('{{LOCALE}}') >= 0: | |
| 101 localesRegExp = re.compile(r'^(.*?){{LOCALE}}(.*?){{LOCALE}}(.*)$', re.M
) | |
| 102 replacement = '\n'.join(map(lambda locale: r'\1%s\2%s\3' % (locale, loca
le), params['locales'])) | |
| 103 data = re.sub(localesRegExp, replacement, data) | |
| 104 | |
| 105 return data | |
| 106 | |
| 107 | |
| 108 def readLocaleMetadata(baseDir, locales): | |
| 109 result = {} | |
| 110 | |
| 111 # Make sure we always have fallback data even if the default locale isn't pa
rt | |
| 112 # of the build | |
| 113 locales = list(locales) | |
| 114 if not defaultLocale in locales: | |
| 115 locales.append(defaultLocale) | |
| 116 | |
| 117 for locale in locales: | |
| 118 data = SafeConfigParser() | |
| 119 data.optionxform = str | |
| 120 try: | |
| 121 result[locale] = localeTools.readFile(os.path.join(getLocalesDir(bas
eDir), locale, 'meta.properties')) | |
| 122 except: | |
| 123 result[locale] = {} | |
| 124 return result | |
| 125 | |
| 126 | |
| 127 def getContributors(metadata): | |
| 128 main = [] | |
| 129 additional = set() | |
| 130 if metadata.has_section('contributors'): | |
| 131 options = metadata.options('contributors') | |
| 132 options.sort() | |
| 133 for option in options: | |
| 134 value = metadata.get('contributors', option) | |
| 135 if re.search(r'\D', option): | |
| 136 match = re.search(r'^\s*(\S+)\s+//([^/\s]+)/@(\S+)\s*$', value) | |
| 137 if not match: | |
| 138 print >>sys.stderr, 'Warning: unrecognized contributor locat
ion "%s"\n' % value | |
| 139 continue | |
| 140 baseDir = os.path.dirname(metadata.option_source('contributors',
option)) | |
| 141 parts = match.group(1).split('/') | |
| 142 dom = minidom.parse(os.path.join(baseDir, *parts)) | |
| 143 tags = dom.getElementsByTagName(match.group(2)) | |
| 144 for tag in tags: | |
| 145 if tag.hasAttribute(match.group(3)): | |
| 146 for name in re.split(r'\s*,\s*', tag.getAttribute(match.
group(3))): | |
| 147 additional.add(name) | |
| 148 else: | |
| 149 main.append(value) | |
| 150 return main + sorted(additional, key=unicode.lower) | |
| 151 | |
| 152 | |
| 153 def initTranslators(localeMetadata): | |
| 154 for locale in localeMetadata.itervalues(): | |
| 155 if 'translator' in locale: | |
| 156 locale['translators'] = sorted(map(lambda t: t.strip(), locale['tran
slator'].split(',')), key=unicode.lower) | |
| 157 else: | |
| 158 locale['translators'] = [] | |
| 159 | |
| 160 | |
| 161 def createManifest(params): | |
| 162 global KNOWN_APPS, defaultLocale | |
| 163 template = getTemplate('install.rdf.tmpl', autoEscape=True) | |
| 164 templateData = dict(params) | |
| 165 templateData['localeMetadata'] = readLocaleMetadata(params['baseDir'], param
s['locales']) | |
| 166 initTranslators(templateData['localeMetadata']) | |
| 167 templateData['KNOWN_APPS'] = KNOWN_APPS | |
| 168 templateData['defaultLocale'] = defaultLocale | |
| 169 return template.render(templateData).encode('utf-8') | |
| 170 | |
| 171 | |
| 172 def importLocales(params, files): | |
| 173 SECTION = 'import_locales' | |
| 174 if not params['metadata'].has_section(SECTION): | |
| 175 return | |
| 176 | |
| 177 import localeTools | |
| 178 | |
| 179 for locale in params['locales']: | |
| 180 for item in params['metadata'].items(SECTION): | |
| 181 path, keys = item | |
| 182 parts = [locale if p == '*' else p for p in path.split('/')] | |
| 183 source = os.path.join(os.path.dirname(item.source), *parts) | |
| 184 if not os.path.exists(source): | |
| 185 continue | |
| 186 | |
| 187 with io.open(source, 'r', encoding='utf-8') as handle: | |
| 188 data = json.load(handle) | |
| 189 | |
| 190 target_name = os.path.splitext(os.path.basename(source))[0] + '.prop
erties' | |
| 191 target = archive_path(os.path.join(getLocalesDir(params['baseDir']),
locale, target_name), params['baseDir']) | |
| 192 | |
| 193 files[target] = '' | |
| 194 for key, value in sorted(data.items()): | |
| 195 message = value['message'] | |
| 196 files[target] += localeTools.generateStringEntry(key, message, t
arget).encode('utf-8') | |
| 197 | |
| 198 | |
| 199 def fixupLocales(params, files): | |
| 200 global defaultLocale | |
| 201 | |
| 202 # Read in default locale data, it might not be included in package files | |
| 203 defaultLocaleDir = os.path.join(getLocalesDir(params['baseDir']), defaultLoc
ale) | |
| 204 reference_files = Files(getPackageFiles(params), getIgnoredFiles(params)) | |
| 205 reference_files.read(defaultLocaleDir, archive_path(defaultLocaleDir, params
['baseDir'])) | |
| 206 reference_params = dict(params) | |
| 207 reference_params['locales'] = [defaultLocale] | |
| 208 importLocales(reference_params, reference_files) | |
| 209 | |
| 210 reference = {} | |
| 211 for path, data in reference_files.iteritems(): | |
| 212 filename = path.split('/')[-1] | |
| 213 data = localeTools.parseString(data.decode('utf-8'), filename) | |
| 214 if data: | |
| 215 reference[filename] = data | |
| 216 | |
| 217 for locale in params['locales']: | |
| 218 for file in reference.iterkeys(): | |
| 219 path = 'chrome/locale/%s/%s' % (locale, file) | |
| 220 if path in files: | |
| 221 data = localeTools.parseString(files[path].decode('utf-8'), path
) | |
| 222 for key, value in reference[file].iteritems(): | |
| 223 if not key in data: | |
| 224 files[path] += localeTools.generateStringEntry(key, valu
e, path).encode('utf-8') | |
| 225 else: | |
| 226 files[path] = reference[file]['_origData'].encode('utf-8') | |
| 227 | |
| 228 | |
| 229 def processJSONFiles(params, files): | |
| 230 prefix = 'lib/' | |
| 231 for name, content in files.iteritems(): | |
| 232 if name.startswith(prefix) and name.endswith('.json'): | |
| 233 params['jsonRequires'][name[len(prefix):]] = json.loads(content) | |
| 234 for name in params['jsonRequires'].iterkeys(): | |
| 235 del files[prefix + name] | |
| 236 | |
| 237 | |
| 238 def addMissingFiles(params, files): | |
| 239 templateData = { | |
| 240 'hasChrome': False, | |
| 241 'hasChromeRequires': False, | |
| 242 'hasShutdownHandlers': False, | |
| 243 'chromeWindows': [], | |
| 244 'requires': set(), | |
| 245 'jsonRequires': params['jsonRequires'], | |
| 246 'metadata': params['metadata'], | |
| 247 'hasWebExtension': params['hasWebExtension'], | |
| 248 'multicompartment': params['multicompartment'], | |
| 249 'applications': dict((v, k) for k, v in KNOWN_APPS.iteritems()), | |
| 250 } | |
| 251 | |
| 252 def checkScript(name): | |
| 253 content = files[name] | |
| 254 for match in re.finditer(r'(?:^|\s)require\(\s*"([\w\-]+)"\s*\)', conten
t): | |
| 255 templateData['requires'].add(match.group(1)) | |
| 256 if name.startswith('chrome/content/'): | |
| 257 templateData['hasChromeRequires'] = True | |
| 258 if not '/' in name or name.startswith('lib/'): | |
| 259 if re.search(r'(?:^|\s)onShutdown\.', content): | |
| 260 templateData['hasShutdownHandlers'] = True | |
| 261 | |
| 262 for name, content in files.iteritems(): | |
| 263 if name == 'chrome.manifest': | |
| 264 templateData['hasChrome'] = True | |
| 265 elif name.endswith('.js'): | |
| 266 checkScript(name) | |
| 267 elif name.endswith('.xul'): | |
| 268 match = re.search(r'<(?:window|dialog)\s[^>]*\bwindowtype="([^">]+)"
', content) | |
| 269 if match: | |
| 270 templateData['chromeWindows'].append(match.group(1)) | |
| 271 | |
| 272 while True: | |
| 273 missing = [] | |
| 274 for module in templateData['requires']: | |
| 275 moduleFile = 'lib/' + module + '.js' | |
| 276 if not moduleFile in files: | |
| 277 import buildtools | |
| 278 path = os.path.join(buildtools.__path__[0], moduleFile) | |
| 279 if os.path.exists(path): | |
| 280 missing.append((path, moduleFile)) | |
| 281 if not len(missing): | |
| 282 break | |
| 283 for path, moduleFile in missing: | |
| 284 files.read(path, moduleFile) | |
| 285 checkScript(moduleFile) | |
| 286 | |
| 287 template = getTemplate('bootstrap.js.tmpl') | |
| 288 files['bootstrap.js'] = template.render(templateData).encode('utf-8') | |
| 289 | |
| 290 | |
| 291 def createBuild(baseDir, type='gecko', outFile=None, locales=None, buildNum=None
, releaseBuild=False, multicompartment=False): | |
| 292 if locales == None: | |
| 293 locales = getLocales(baseDir) | |
| 294 elif locales == 'all': | |
| 295 locales = getLocales(baseDir, True) | |
| 296 | |
| 297 metadata = readMetadata(baseDir, type) | |
| 298 version = getBuildVersion(baseDir, metadata, releaseBuild, buildNum) | |
| 299 | |
| 300 if outFile == None: | |
| 301 outFile = getDefaultFileName(metadata, version, 'xpi') | |
| 302 | |
| 303 contributors = getContributors(metadata) | |
| 304 | |
| 305 params = { | |
| 306 'baseDir': baseDir, | |
| 307 'locales': locales, | |
| 308 'releaseBuild': releaseBuild, | |
| 309 'version': version.encode('utf-8'), | |
| 310 'metadata': metadata, | |
| 311 'contributors': contributors, | |
| 312 'multicompartment': multicompartment, | |
| 313 'hasWebExtension': os.path.isdir(os.path.join(baseDir, 'webextension')), | |
| 314 'jsonRequires': {}, | |
| 315 } | |
| 316 | |
| 317 mapped = metadata.items('mapping') if metadata.has_section('mapping') else [
] | |
| 318 skip = [opt for opt, _ in mapped] + ['chrome'] | |
| 319 files = Files(getPackageFiles(params), getIgnoredFiles(params), | |
| 320 process=lambda path, data: processFile(path, data, params)) | |
| 321 files['install.rdf'] = createManifest(params) | |
| 322 files.readMappedFiles(mapped) | |
| 323 files.read(baseDir, skip=skip) | |
| 324 for name, path in getChromeSubdirs(baseDir, params['locales']).iteritems(): | |
| 325 if os.path.isdir(path): | |
| 326 files.read(path, 'chrome/%s' % name, skip=skip) | |
| 327 importLocales(params, files) | |
| 328 fixupLocales(params, files) | |
| 329 processJSONFiles(params, files) | |
| 330 if not 'bootstrap.js' in files: | |
| 331 addMissingFiles(params, files) | |
| 332 if metadata.has_section('preprocess'): | |
| 333 files.preprocess([f for f, _ in metadata.items('preprocess')]) | |
| 334 files.zip(outFile, sortKey=lambda x: '!' if x == 'META-INF/zigbert.rsa' else
x) | |
| 335 | |
| 336 | |
| 337 def autoInstall(baseDir, type, host, port, multicompartment=False): | |
| 338 fileBuffer = StringIO() | |
| 339 createBuild(baseDir, type=type, outFile=fileBuffer, multicompartment=multico
mpartment) | |
| 340 urllib.urlopen('http://%s:%s/' % (host, port), data=fileBuffer.getvalue()) | |
| OLD | NEW |