PageRenderTime 53ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 0ms

/config/JarMaker.py

https://bitbucket.org/hsoft/mozilla-central
Python | 489 lines | 442 code | 14 blank | 33 comment | 2 complexity | b145b8d4a4a1864106cf86661c96a0ec MD5 | raw file
Possible License(s): JSON, LGPL-2.1, LGPL-3.0, AGPL-1.0, MIT, MPL-2.0-no-copyleft-exception, Apache-2.0, GPL-2.0, BSD-2-Clause, MPL-2.0, BSD-3-Clause, 0BSD
  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. '''jarmaker.py provides a python class to package up chrome content by
  5. processing jar.mn files.
  6. See the documentation for jar.mn on MDC for further details on the format.
  7. '''
  8. import sys
  9. import os
  10. import os.path
  11. import errno
  12. import re
  13. import logging
  14. from time import localtime
  15. from optparse import OptionParser
  16. from MozZipFile import ZipFile
  17. from cStringIO import StringIO
  18. from datetime import datetime
  19. from utils import pushback_iter, lockFile
  20. from Preprocessor import Preprocessor
  21. from buildlist import addEntriesToListFile
  22. if sys.platform == "win32":
  23. from ctypes import windll, WinError
  24. CreateHardLink = windll.kernel32.CreateHardLinkA
  25. __all__ = ['JarMaker']
  26. class ZipEntry:
  27. '''Helper class for jar output.
  28. This class defines a simple file-like object for a zipfile.ZipEntry
  29. so that we can consecutively write to it and then close it.
  30. This methods hooks into ZipFile.writestr on close().
  31. '''
  32. def __init__(self, name, zipfile):
  33. self._zipfile = zipfile
  34. self._name = name
  35. self._inner = StringIO()
  36. def write(self, content):
  37. 'Append the given content to this zip entry'
  38. self._inner.write(content)
  39. return
  40. def close(self):
  41. 'The close method writes the content back to the zip file.'
  42. self._zipfile.writestr(self._name, self._inner.getvalue())
  43. def getModTime(aPath):
  44. if not os.path.isfile(aPath):
  45. return 0
  46. mtime = os.stat(aPath).st_mtime
  47. return localtime(mtime)
  48. class JarMaker(object):
  49. '''JarMaker reads jar.mn files and process those into jar files or
  50. flat directories, along with chrome.manifest files.
  51. '''
  52. ignore = re.compile('\s*(\#.*)?$')
  53. jarline = re.compile('(?:(?P<jarfile>[\w\d.\-\_\\\/]+).jar\:)|(?:\s*(\#.*)?)\s*$')
  54. regline = re.compile('\%\s+(.*)$')
  55. entryre = '(?P<optPreprocess>\*)?(?P<optOverwrite>\+?)\s+'
  56. entryline = re.compile(entryre + '(?P<output>[\w\d.\-\_\\\/\+\@]+)\s*(\((?P<locale>\%?)(?P<source>[\w\d.\-\_\\\/\@]+)\))?\s*$')
  57. def __init__(self, outputFormat = 'flat', useJarfileManifest = True,
  58. useChromeManifest = False):
  59. self.outputFormat = outputFormat
  60. self.useJarfileManifest = useJarfileManifest
  61. self.useChromeManifest = useChromeManifest
  62. self.pp = Preprocessor()
  63. def getCommandLineParser(self):
  64. '''Get a optparse.OptionParser for jarmaker.
  65. This OptionParser has the options for jarmaker as well as
  66. the options for the inner PreProcessor.
  67. '''
  68. # HACK, we need to unescape the string variables we get,
  69. # the perl versions didn't grok strings right
  70. p = self.pp.getCommandLineParser(unescapeDefines = True)
  71. p.add_option('-f', type="choice", default="jar",
  72. choices=('jar', 'flat', 'symlink'),
  73. help="fileformat used for output", metavar="[jar, flat, symlink]")
  74. p.add_option('-v', action="store_true", dest="verbose",
  75. help="verbose output")
  76. p.add_option('-q', action="store_false", dest="verbose",
  77. help="verbose output")
  78. p.add_option('-e', action="store_true",
  79. help="create chrome.manifest instead of jarfile.manifest")
  80. p.add_option('--both-manifests', action="store_true",
  81. dest="bothManifests",
  82. help="create chrome.manifest and jarfile.manifest")
  83. p.add_option('-s', type="string", action="append", default=[],
  84. help="source directory")
  85. p.add_option('-t', type="string",
  86. help="top source directory")
  87. p.add_option('-c', '--l10n-src', type="string", action="append",
  88. help="localization directory")
  89. p.add_option('--l10n-base', type="string", action="append", default=[],
  90. help="base directory to be used for localization (multiple)")
  91. p.add_option('-j', type="string",
  92. help="jarfile directory")
  93. # backwards compat, not needed
  94. p.add_option('-a', action="store_false", default=True,
  95. help="NOT SUPPORTED, turn auto-registration of chrome off (installed-chrome.txt)")
  96. p.add_option('-d', type="string",
  97. help="UNUSED, chrome directory")
  98. p.add_option('-o', help="cross compile for auto-registration, ignored")
  99. p.add_option('-l', action="store_true",
  100. help="ignored (used to switch off locks)")
  101. p.add_option('-x', action="store_true",
  102. help="force Unix")
  103. p.add_option('-z', help="backwards compat, ignored")
  104. p.add_option('-p', help="backwards compat, ignored")
  105. return p
  106. def processIncludes(self, includes):
  107. '''Process given includes with the inner PreProcessor.
  108. Only use this for #defines, the includes shouldn't generate
  109. content.
  110. '''
  111. self.pp.out = StringIO()
  112. for inc in includes:
  113. self.pp.do_include(inc)
  114. includesvalue = self.pp.out.getvalue()
  115. if includesvalue:
  116. logging.info("WARNING: Includes produce non-empty output")
  117. self.pp.out = None
  118. pass
  119. def finalizeJar(self, jarPath, chromebasepath, register,
  120. doZip=True):
  121. '''Helper method to write out the chrome registration entries to
  122. jarfile.manifest or chrome.manifest, or both.
  123. The actual file processing is done in updateManifest.
  124. '''
  125. # rewrite the manifest, if entries given
  126. if not register:
  127. return
  128. chromeManifest = os.path.join(os.path.dirname(jarPath),
  129. '..', 'chrome.manifest')
  130. if self.useJarfileManifest:
  131. self.updateManifest(jarPath + '.manifest', chromebasepath % '',
  132. register)
  133. addEntriesToListFile(chromeManifest, ['manifest chrome/%s.manifest' % (os.path.basename(jarPath),)])
  134. if self.useChromeManifest:
  135. self.updateManifest(chromeManifest, chromebasepath % 'chrome/',
  136. register)
  137. def updateManifest(self, manifestPath, chromebasepath, register):
  138. '''updateManifest replaces the % in the chrome registration entries
  139. with the given chrome base path, and updates the given manifest file.
  140. '''
  141. lock = lockFile(manifestPath + '.lck')
  142. try:
  143. myregister = dict.fromkeys(map(lambda s: s.replace('%', chromebasepath),
  144. register.iterkeys()))
  145. manifestExists = os.path.isfile(manifestPath)
  146. mode = (manifestExists and 'r+b') or 'wb'
  147. mf = open(manifestPath, mode)
  148. if manifestExists:
  149. # import previous content into hash, ignoring empty ones and comments
  150. imf = re.compile('(#.*)?$')
  151. for l in re.split('[\r\n]+', mf.read()):
  152. if imf.match(l):
  153. continue
  154. myregister[l] = None
  155. mf.seek(0)
  156. for k in myregister.iterkeys():
  157. mf.write(k + os.linesep)
  158. mf.close()
  159. finally:
  160. lock = None
  161. def makeJar(self, infile=None,
  162. jardir='',
  163. sourcedirs=[], topsourcedir='', localedirs=None):
  164. '''makeJar is the main entry point to JarMaker.
  165. It takes the input file, the output directory, the source dirs and the
  166. top source dir as argument, and optionally the l10n dirs.
  167. '''
  168. if isinstance(infile, basestring):
  169. logging.info("processing " + infile)
  170. pp = self.pp.clone()
  171. pp.out = StringIO()
  172. pp.do_include(infile)
  173. lines = pushback_iter(pp.out.getvalue().splitlines())
  174. try:
  175. while True:
  176. l = lines.next()
  177. m = self.jarline.match(l)
  178. if not m:
  179. raise RuntimeError(l)
  180. if m.group('jarfile') is None:
  181. # comment
  182. continue
  183. self.processJarSection(m.group('jarfile'), lines,
  184. jardir, sourcedirs, topsourcedir,
  185. localedirs)
  186. except StopIteration:
  187. # we read the file
  188. pass
  189. return
  190. def makeJars(self, infiles, l10nbases,
  191. jardir='',
  192. sourcedirs=[], topsourcedir='', localedirs=None):
  193. '''makeJars is the second main entry point to JarMaker.
  194. It takes an iterable sequence of input file names, the l10nbases,
  195. the output directory, the source dirs and the
  196. top source dir as argument, and optionally the l10n dirs.
  197. It iterates over all inputs, guesses srcdir and l10ndir from the
  198. path and topsourcedir and calls into makeJar.
  199. The l10ndirs are created by guessing the relativesrcdir, and resolving
  200. that against the l10nbases. l10nbases can either be path strings, or
  201. callables. In the latter case, that will be called with the
  202. relativesrcdir as argument, and is expected to return a path string.
  203. This logic is disabled if the jar.mn path is not inside the topsrcdir.
  204. '''
  205. topsourcedir = os.path.normpath(os.path.abspath(topsourcedir))
  206. def resolveL10nBase(relpath):
  207. def _resolve(base):
  208. if isinstance(base, basestring):
  209. return os.path.join(base, relpath)
  210. if callable(base):
  211. return base(relpath)
  212. return base
  213. return _resolve
  214. for infile in infiles:
  215. srcdir = os.path.normpath(os.path.abspath(os.path.dirname(infile)))
  216. l10ndir = srcdir
  217. if os.path.basename(srcdir) == 'locales':
  218. l10ndir = os.path.dirname(l10ndir)
  219. l10ndirs = None
  220. # srcdir may not be a child of topsourcedir, in which case
  221. # we assume that the caller passed in suitable sourcedirs,
  222. # and just skip passing in localedirs
  223. if srcdir.startswith(topsourcedir):
  224. rell10ndir = l10ndir[len(topsourcedir):].lstrip(os.sep)
  225. l10ndirs = map(resolveL10nBase(rell10ndir), l10nbases)
  226. if localedirs is not None:
  227. l10ndirs += [os.path.normpath(os.path.abspath(s))
  228. for s in localedirs]
  229. srcdirs = [os.path.normpath(os.path.abspath(s))
  230. for s in sourcedirs] + [srcdir]
  231. self.makeJar(infile=infile,
  232. sourcedirs=srcdirs, topsourcedir=topsourcedir,
  233. localedirs=l10ndirs,
  234. jardir=jardir)
  235. def processJarSection(self, jarfile, lines,
  236. jardir, sourcedirs, topsourcedir, localedirs):
  237. '''Internal method called by makeJar to actually process a section
  238. of a jar.mn file.
  239. jarfile is the basename of the jarfile or the directory name for
  240. flat output, lines is a pushback_iterator of the lines of jar.mn,
  241. the remaining options are carried over from makeJar.
  242. '''
  243. # chromebasepath is used for chrome registration manifests
  244. # %s is getting replaced with chrome/ for chrome.manifest, and with
  245. # an empty string for jarfile.manifest
  246. chromebasepath = '%s' + os.path.basename(jarfile)
  247. if self.outputFormat == 'jar':
  248. chromebasepath = 'jar:' + chromebasepath + '.jar!'
  249. chromebasepath += '/'
  250. jarfile = os.path.join(jardir, jarfile)
  251. jf = None
  252. if self.outputFormat == 'jar':
  253. #jar
  254. jarfilepath = jarfile + '.jar'
  255. try:
  256. os.makedirs(os.path.dirname(jarfilepath))
  257. except OSError, error:
  258. if error.errno != errno.EEXIST:
  259. raise
  260. jf = ZipFile(jarfilepath, 'a', lock = True)
  261. outHelper = self.OutputHelper_jar(jf)
  262. else:
  263. outHelper = getattr(self, 'OutputHelper_' + self.outputFormat)(jarfile)
  264. register = {}
  265. # This loop exits on either
  266. # - the end of the jar.mn file
  267. # - an line in the jar.mn file that's not part of a jar section
  268. # - on an exception raised, close the jf in that case in a finally
  269. try:
  270. while True:
  271. try:
  272. l = lines.next()
  273. except StopIteration:
  274. # we're done with this jar.mn, and this jar section
  275. self.finalizeJar(jarfile, chromebasepath, register)
  276. if jf is not None:
  277. jf.close()
  278. # reraise the StopIteration for makeJar
  279. raise
  280. if self.ignore.match(l):
  281. continue
  282. m = self.regline.match(l)
  283. if m:
  284. rline = m.group(1)
  285. register[rline] = 1
  286. continue
  287. m = self.entryline.match(l)
  288. if not m:
  289. # neither an entry line nor chrome reg, this jar section is done
  290. self.finalizeJar(jarfile, chromebasepath, register)
  291. if jf is not None:
  292. jf.close()
  293. lines.pushback(l)
  294. return
  295. self._processEntryLine(m, sourcedirs, topsourcedir, localedirs,
  296. outHelper, jf)
  297. finally:
  298. if jf is not None:
  299. jf.close()
  300. return
  301. def _processEntryLine(self, m,
  302. sourcedirs, topsourcedir, localedirs,
  303. outHelper, jf):
  304. out = m.group('output')
  305. src = m.group('source') or os.path.basename(out)
  306. # pick the right sourcedir -- l10n, topsrc or src
  307. if m.group('locale'):
  308. src_base = localedirs
  309. elif src.startswith('/'):
  310. # path/in/jar/file_name.xul (/path/in/sourcetree/file_name.xul)
  311. # refers to a path relative to topsourcedir, use that as base
  312. # and strip the leading '/'
  313. src_base = [topsourcedir]
  314. src = src[1:]
  315. else:
  316. # use srcdirs and the objdir (current working dir) for relative paths
  317. src_base = sourcedirs + [os.getcwd()]
  318. # check if the source file exists
  319. realsrc = None
  320. for _srcdir in src_base:
  321. if os.path.isfile(os.path.join(_srcdir, src)):
  322. realsrc = os.path.join(_srcdir, src)
  323. break
  324. if realsrc is None:
  325. if jf is not None:
  326. jf.close()
  327. raise RuntimeError('File "%s" not found in %s' % (src, ', '.join(src_base)))
  328. if m.group('optPreprocess'):
  329. outf = outHelper.getOutput(out)
  330. inf = open(realsrc)
  331. pp = self.pp.clone()
  332. if src[-4:] == '.css':
  333. pp.setMarker('%')
  334. pp.out = outf
  335. pp.do_include(inf)
  336. pp.warnUnused(realsrc)
  337. outf.close()
  338. inf.close()
  339. return
  340. # copy or symlink if newer or overwrite
  341. if (m.group('optOverwrite')
  342. or (getModTime(realsrc) >
  343. outHelper.getDestModTime(m.group('output')))):
  344. if self.outputFormat == 'symlink':
  345. outHelper.symlink(realsrc, out)
  346. return
  347. outf = outHelper.getOutput(out)
  348. # open in binary mode, this can be images etc
  349. inf = open(realsrc, 'rb')
  350. outf.write(inf.read())
  351. outf.close()
  352. inf.close()
  353. class OutputHelper_jar(object):
  354. '''Provide getDestModTime and getOutput for a given jarfile.
  355. '''
  356. def __init__(self, jarfile):
  357. self.jarfile = jarfile
  358. def getDestModTime(self, aPath):
  359. try :
  360. info = self.jarfile.getinfo(aPath)
  361. return info.date_time
  362. except:
  363. return 0
  364. def getOutput(self, name):
  365. return ZipEntry(name, self.jarfile)
  366. class OutputHelper_flat(object):
  367. '''Provide getDestModTime and getOutput for a given flat
  368. output directory. The helper method ensureDirFor is used by
  369. the symlink subclass.
  370. '''
  371. def __init__(self, basepath):
  372. self.basepath = basepath
  373. def getDestModTime(self, aPath):
  374. return getModTime(os.path.join(self.basepath, aPath))
  375. def getOutput(self, name):
  376. out = self.ensureDirFor(name)
  377. # remove previous link or file
  378. try:
  379. os.remove(out)
  380. except OSError, e:
  381. if e.errno != errno.ENOENT:
  382. raise
  383. return open(out, 'wb')
  384. def ensureDirFor(self, name):
  385. out = os.path.join(self.basepath, name)
  386. outdir = os.path.dirname(out)
  387. if not os.path.isdir(outdir):
  388. try:
  389. os.makedirs(outdir)
  390. except OSError, error:
  391. if error.errno != errno.EEXIST:
  392. raise
  393. return out
  394. class OutputHelper_symlink(OutputHelper_flat):
  395. '''Subclass of OutputHelper_flat that provides a helper for
  396. creating a symlink including creating the parent directories.
  397. '''
  398. def symlink(self, src, dest):
  399. out = self.ensureDirFor(dest)
  400. # remove previous link or file
  401. try:
  402. os.remove(out)
  403. except OSError, e:
  404. if e.errno != errno.ENOENT:
  405. raise
  406. if sys.platform != "win32":
  407. os.symlink(src, out)
  408. else:
  409. # On Win32, use ctypes to create a hardlink
  410. rv = CreateHardLink(out, src, None)
  411. if rv == 0:
  412. raise WinError()
  413. def main():
  414. jm = JarMaker()
  415. p = jm.getCommandLineParser()
  416. (options, args) = p.parse_args()
  417. jm.processIncludes(options.I)
  418. jm.outputFormat = options.f
  419. if options.e:
  420. jm.useChromeManifest = True
  421. jm.useJarfileManifest = False
  422. if options.bothManifests:
  423. jm.useChromeManifest = True
  424. jm.useJarfileManifest = True
  425. noise = logging.INFO
  426. if options.verbose is not None:
  427. noise = (options.verbose and logging.DEBUG) or logging.WARN
  428. if sys.version_info[:2] > (2,3):
  429. logging.basicConfig(format = "%(message)s")
  430. else:
  431. logging.basicConfig()
  432. logging.getLogger().setLevel(noise)
  433. topsrc = options.t
  434. topsrc = os.path.normpath(os.path.abspath(topsrc))
  435. if not args:
  436. jm.makeJar(infile=sys.stdin,
  437. sourcedirs=options.s, topsourcedir=topsrc,
  438. localedirs=options.l10n_src,
  439. jardir=options.j)
  440. else:
  441. jm.makeJars(args, options.l10n_base,
  442. jardir=options.j,
  443. sourcedirs=options.s, topsourcedir=topsrc,
  444. localedirs=options.l10n_src)
  445. if __name__ == "__main__":
  446. main()