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 |