PageRenderTime 59ms CodeModel.GetById 26ms RepoModel.GetById 1ms app.codeStats 0ms

/config/JarMaker.py

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