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/.
5
'''jarmaker.py provides a python class to package up chrome content by
6
processing jar.mn files.
8
See the documentation for jar.mn on MDC for further details on the format.
17
from time import localtime
18
from optparse import OptionParser
19
from MozZipFile import ZipFile
20
from cStringIO import StringIO
21
from datetime import datetime
23
from utils import pushback_iter, lockFile
24
from Preprocessor import Preprocessor
25
from buildlist import addEntriesToListFile
26
if sys.platform == "win32":
27
from ctypes import windll, WinError
28
CreateHardLink = windll.kernel32.CreateHardLinkA
30
__all__ = ['JarMaker']
33
'''Helper class for jar output.
35
This class defines a simple file-like object for a zipfile.ZipEntry
36
so that we can consecutively write to it and then close it.
37
This methods hooks into ZipFile.writestr on close().
39
def __init__(self, name, zipfile):
40
self._zipfile = zipfile
42
self._inner = StringIO()
44
def write(self, content):
45
'Append the given content to this zip entry'
46
self._inner.write(content)
50
'The close method writes the content back to the zip file.'
51
self._zipfile.writestr(self._name, self._inner.getvalue())
53
def getModTime(aPath):
54
if not os.path.isfile(aPath):
56
mtime = os.stat(aPath).st_mtime
57
return localtime(mtime)
60
class JarMaker(object):
61
'''JarMaker reads jar.mn files and process those into jar files or
62
flat directories, along with chrome.manifest files.
65
ignore = re.compile('\s*(\#.*)?$')
66
jarline = re.compile('(?:(?P<jarfile>[\w\d.\-\_\\\/]+).jar\:)|(?:\s*(\#.*)?)\s*$')
67
regline = re.compile('\%\s+(.*)$')
68
entryre = '(?P<optPreprocess>\*)?(?P<optOverwrite>\+?)\s+'
69
entryline = re.compile(entryre + '(?P<output>[\w\d.\-\_\\\/\+]+)\s*(\((?P<locale>\%?)(?P<source>[\w\d.\-\_\\\/]+)\))?\s*$')
71
def __init__(self, outputFormat = 'flat', useJarfileManifest = True,
72
useChromeManifest = False):
73
self.outputFormat = outputFormat
74
self.useJarfileManifest = useJarfileManifest
75
self.useChromeManifest = useChromeManifest
76
self.pp = Preprocessor()
78
def getCommandLineParser(self):
79
'''Get a optparse.OptionParser for jarmaker.
81
This OptionParser has the options for jarmaker as well as
82
the options for the inner PreProcessor.
84
# HACK, we need to unescape the string variables we get,
85
# the perl versions didn't grok strings right
86
p = self.pp.getCommandLineParser(unescapeDefines = True)
87
p.add_option('-f', type="choice", default="jar",
88
choices=('jar', 'flat', 'symlink'),
89
help="fileformat used for output", metavar="[jar, flat, symlink]")
90
p.add_option('-v', action="store_true", dest="verbose",
91
help="verbose output")
92
p.add_option('-q', action="store_false", dest="verbose",
93
help="verbose output")
94
p.add_option('-e', action="store_true",
95
help="create chrome.manifest instead of jarfile.manifest")
96
p.add_option('--both-manifests', action="store_true",
98
help="create chrome.manifest and jarfile.manifest")
99
p.add_option('-s', type="string", action="append", default=[],
100
help="source directory")
101
p.add_option('-t', type="string",
102
help="top source directory")
103
p.add_option('-c', '--l10n-src', type="string", action="append",
104
help="localization directory")
105
p.add_option('--l10n-base', type="string", action="append", default=[],
106
help="base directory to be used for localization (multiple)")
107
p.add_option('-j', type="string",
108
help="jarfile directory")
109
# backwards compat, not needed
110
p.add_option('-a', action="store_false", default=True,
111
help="NOT SUPPORTED, turn auto-registration of chrome off (installed-chrome.txt)")
112
p.add_option('-d', type="string",
113
help="UNUSED, chrome directory")
114
p.add_option('-o', help="cross compile for auto-registration, ignored")
115
p.add_option('-l', action="store_true",
116
help="ignored (used to switch off locks)")
117
p.add_option('-x', action="store_true",
119
p.add_option('-z', help="backwards compat, ignored")
120
p.add_option('-p', help="backwards compat, ignored")
123
def processIncludes(self, includes):
124
'''Process given includes with the inner PreProcessor.
126
Only use this for #defines, the includes shouldn't generate
129
self.pp.out = StringIO()
131
self.pp.do_include(inc)
132
includesvalue = self.pp.out.getvalue()
134
logging.info("WARNING: Includes produce non-empty output")
138
def finalizeJar(self, jarPath, chromebasepath, register,
140
'''Helper method to write out the chrome registration entries to
141
jarfile.manifest or chrome.manifest, or both.
143
The actual file processing is done in updateManifest.
145
# rewrite the manifest, if entries given
149
chromeManifest = os.path.join(os.path.dirname(jarPath),
150
'..', 'chrome.manifest')
152
if self.useJarfileManifest:
153
self.updateManifest(jarPath + '.manifest', chromebasepath % '',
155
addEntriesToListFile(chromeManifest, ['manifest chrome/%s.manifest' % (os.path.basename(jarPath),)])
156
if self.useChromeManifest:
157
self.updateManifest(chromeManifest, chromebasepath % 'chrome/',
160
def updateManifest(self, manifestPath, chromebasepath, register):
161
'''updateManifest replaces the % in the chrome registration entries
162
with the given chrome base path, and updates the given manifest file.
164
lock = lockFile(manifestPath + '.lck')
166
myregister = dict.fromkeys(map(lambda s: s.replace('%', chromebasepath),
167
register.iterkeys()))
168
manifestExists = os.path.isfile(manifestPath)
169
mode = (manifestExists and 'r+b') or 'wb'
170
mf = open(manifestPath, mode)
172
# import previous content into hash, ignoring empty ones and comments
173
imf = re.compile('(#.*)?$')
174
for l in re.split('[\r\n]+', mf.read()):
179
for k in myregister.iterkeys():
180
mf.write(k + os.linesep)
185
def makeJar(self, infile=None,
187
sourcedirs=[], topsourcedir='', localedirs=None):
188
'''makeJar is the main entry point to JarMaker.
190
It takes the input file, the output directory, the source dirs and the
191
top source dir as argument, and optionally the l10n dirs.
193
if isinstance(infile, basestring):
194
logging.info("processing " + infile)
197
pp.do_include(infile)
198
lines = pushback_iter(pp.out.getvalue().splitlines())
202
m = self.jarline.match(l)
204
raise RuntimeError(l)
205
if m.group('jarfile') is None:
208
self.processJarSection(m.group('jarfile'), lines,
209
jardir, sourcedirs, topsourcedir,
211
except StopIteration:
216
def makeJars(self, infiles, l10nbases,
218
sourcedirs=[], topsourcedir='', localedirs=None):
219
'''makeJars is the second main entry point to JarMaker.
221
It takes an iterable sequence of input file names, the l10nbases,
222
the output directory, the source dirs and the
223
top source dir as argument, and optionally the l10n dirs.
225
It iterates over all inputs, guesses srcdir and l10ndir from the
226
path and topsourcedir and calls into makeJar.
228
The l10ndirs are created by guessing the relativesrcdir, and resolving
229
that against the l10nbases. l10nbases can either be path strings, or
230
callables. In the latter case, that will be called with the
231
relativesrcdir as argument, and is expected to return a path string.
232
This logic is disabled if the jar.mn path is not inside the topsrcdir.
234
topsourcedir = os.path.normpath(os.path.abspath(topsourcedir))
235
def resolveL10nBase(relpath):
237
if isinstance(base, basestring):
238
return os.path.join(base, relpath)
243
for infile in infiles:
244
srcdir = os.path.normpath(os.path.abspath(os.path.dirname(infile)))
246
if os.path.basename(srcdir) == 'locales':
247
l10ndir = os.path.dirname(l10ndir)
250
# srcdir may not be a child of topsourcedir, in which case
251
# we assume that the caller passed in suitable sourcedirs,
252
# and just skip passing in localedirs
253
if srcdir.startswith(topsourcedir):
254
rell10ndir = l10ndir[len(topsourcedir):].lstrip(os.sep)
256
l10ndirs = map(resolveL10nBase(rell10ndir), l10nbases)
257
if localedirs is not None:
258
l10ndirs += [os.path.normpath(os.path.abspath(s))
260
srcdirs = [os.path.normpath(os.path.abspath(s))
261
for s in sourcedirs] + [srcdir]
262
self.makeJar(infile=infile,
263
sourcedirs=srcdirs, topsourcedir=topsourcedir,
268
def processJarSection(self, jarfile, lines,
269
jardir, sourcedirs, topsourcedir, localedirs):
270
'''Internal method called by makeJar to actually process a section
273
jarfile is the basename of the jarfile or the directory name for
274
flat output, lines is a pushback_iterator of the lines of jar.mn,
275
the remaining options are carried over from makeJar.
278
# chromebasepath is used for chrome registration manifests
279
# %s is getting replaced with chrome/ for chrome.manifest, and with
280
# an empty string for jarfile.manifest
281
chromebasepath = '%s' + os.path.basename(jarfile)
282
if self.outputFormat == 'jar':
283
chromebasepath = 'jar:' + chromebasepath + '.jar!'
284
chromebasepath += '/'
286
jarfile = os.path.join(jardir, jarfile)
288
if self.outputFormat == 'jar':
290
jarfilepath = jarfile + '.jar'
292
os.makedirs(os.path.dirname(jarfilepath))
293
except OSError, error:
294
if error.errno != errno.EEXIST:
296
jf = ZipFile(jarfilepath, 'a', lock = True)
297
outHelper = self.OutputHelper_jar(jf)
299
outHelper = getattr(self, 'OutputHelper_' + self.outputFormat)(jarfile)
301
# This loop exits on either
302
# - the end of the jar.mn file
303
# - an line in the jar.mn file that's not part of a jar section
304
# - on an exception raised, close the jf in that case in a finally
309
except StopIteration:
310
# we're done with this jar.mn, and this jar section
311
self.finalizeJar(jarfile, chromebasepath, register)
314
# reraise the StopIteration for makeJar
316
if self.ignore.match(l):
318
m = self.regline.match(l)
323
m = self.entryline.match(l)
325
# neither an entry line nor chrome reg, this jar section is done
326
self.finalizeJar(jarfile, chromebasepath, register)
331
self._processEntryLine(m, sourcedirs, topsourcedir, localedirs,
338
def _processEntryLine(self, m,
339
sourcedirs, topsourcedir, localedirs,
341
out = m.group('output')
342
src = m.group('source') or os.path.basename(out)
343
# pick the right sourcedir -- l10n, topsrc or src
344
if m.group('locale'):
345
src_base = localedirs
346
elif src.startswith('/'):
347
# path/in/jar/file_name.xul (/path/in/sourcetree/file_name.xul)
348
# refers to a path relative to topsourcedir, use that as base
349
# and strip the leading '/'
350
src_base = [topsourcedir]
353
# use srcdirs and the objdir (current working dir) for relative paths
354
src_base = sourcedirs + [os.getcwd()]
355
# check if the source file exists
357
for _srcdir in src_base:
358
if os.path.isfile(os.path.join(_srcdir, src)):
359
realsrc = os.path.join(_srcdir, src)
364
raise RuntimeError('File "%s" not found in %s' % (src, ', '.join(src_base)))
365
if m.group('optPreprocess'):
366
outf = outHelper.getOutput(out)
369
if src[-4:] == '.css':
373
pp.warnUnused(realsrc)
377
# copy or symlink if newer or overwrite
378
if (m.group('optOverwrite')
379
or (getModTime(realsrc) >
380
outHelper.getDestModTime(m.group('output')))):
381
if self.outputFormat == 'symlink':
382
outHelper.symlink(realsrc, out)
384
outf = outHelper.getOutput(out)
385
# open in binary mode, this can be images etc
386
inf = open(realsrc, 'rb')
387
outf.write(inf.read())
392
class OutputHelper_jar(object):
393
'''Provide getDestModTime and getOutput for a given jarfile.
395
def __init__(self, jarfile):
396
self.jarfile = jarfile
397
def getDestModTime(self, aPath):
399
info = self.jarfile.getinfo(aPath)
400
return info.date_time
403
def getOutput(self, name):
404
return ZipEntry(name, self.jarfile)
406
class OutputHelper_flat(object):
407
'''Provide getDestModTime and getOutput for a given flat
408
output directory. The helper method ensureDirFor is used by
409
the symlink subclass.
411
def __init__(self, basepath):
412
self.basepath = basepath
413
def getDestModTime(self, aPath):
414
return getModTime(os.path.join(self.basepath, aPath))
415
def getOutput(self, name):
416
out = self.ensureDirFor(name)
417
# remove previous link or file
421
if e.errno != errno.ENOENT:
423
return open(out, 'wb')
424
def ensureDirFor(self, name):
425
out = os.path.join(self.basepath, name)
426
outdir = os.path.dirname(out)
427
if not os.path.isdir(outdir):
430
except OSError, error:
431
if error.errno != errno.EEXIST:
435
class OutputHelper_symlink(OutputHelper_flat):
436
'''Subclass of OutputHelper_flat that provides a helper for
437
creating a symlink including creating the parent directories.
439
def symlink(self, src, dest):
440
out = self.ensureDirFor(dest)
441
# remove previous link or file
445
if e.errno != errno.ENOENT:
447
if sys.platform != "win32":
450
# On Win32, use ctypes to create a hardlink
451
rv = CreateHardLink(out, src, None)
457
p = jm.getCommandLineParser()
458
(options, args) = p.parse_args()
459
jm.processIncludes(options.I)
460
jm.outputFormat = options.f
462
jm.useChromeManifest = True
463
jm.useJarfileManifest = False
464
if options.bothManifests:
465
jm.useChromeManifest = True
466
jm.useJarfileManifest = True
468
if options.verbose is not None:
469
noise = (options.verbose and logging.DEBUG) or logging.WARN
470
if sys.version_info[:2] > (2,3):
471
logging.basicConfig(format = "%(message)s")
473
logging.basicConfig()
474
logging.getLogger().setLevel(noise)
476
topsrc = os.path.normpath(os.path.abspath(topsrc))
478
jm.makeJar(infile=sys.stdin,
479
sourcedirs=options.s, topsourcedir=topsrc,
480
localedirs=options.l10n_src,
483
jm.makeJars(args, options.l10n_base,
485
sourcedirs=options.s, topsourcedir=topsrc,
486
localedirs=options.l10n_src)
488
if __name__ == "__main__":