You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

362 lines
14 KiB

6 years ago
  1. """ xgettext tool
  2. Tool specific initialization of `xgettext` tool.
  3. """
  4. # Copyright (c) 2001 - 2017 The SCons Foundation
  5. #
  6. # Permission is hereby granted, free of charge, to any person obtaining
  7. # a copy of this software and associated documentation files (the
  8. # "Software"), to deal in the Software without restriction, including
  9. # without limitation the rights to use, copy, modify, merge, publish,
  10. # distribute, sublicense, and/or sell copies of the Software, and to
  11. # permit persons to whom the Software is furnished to do so, subject to
  12. # the following conditions:
  13. #
  14. # The above copyright notice and this permission notice shall be included
  15. # in all copies or substantial portions of the Software.
  16. #
  17. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
  18. # KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
  19. # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
  20. # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
  21. # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
  22. # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
  23. # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  24. __revision__ = "src/engine/SCons/Tool/xgettext.py rel_3.0.0:4395:8972f6a2f699 2017/09/18 12:59:24 bdbaddog"
  25. #############################################################################
  26. class _CmdRunner(object):
  27. """ Callable object, which runs shell command storing its stdout and stderr to
  28. variables. It also provides `strfunction()` method, which shall be used by
  29. scons Action objects to print command string. """
  30. def __init__(self, command, commandstr=None):
  31. self.out = None
  32. self.err = None
  33. self.status = None
  34. self.command = command
  35. self.commandstr = commandstr
  36. def __call__(self, target, source, env):
  37. import SCons.Action
  38. import subprocess
  39. import os
  40. import sys
  41. kw = {
  42. 'stdin': 'devnull',
  43. 'stdout': subprocess.PIPE,
  44. 'stderr': subprocess.PIPE,
  45. 'universal_newlines': True,
  46. 'shell': True
  47. }
  48. command = env.subst(self.command, target=target, source=source)
  49. proc = SCons.Action._subproc(env, command, **kw)
  50. self.out, self.err = proc.communicate()
  51. self.status = proc.wait()
  52. if self.err:
  53. sys.stderr.write(unicode(self.err))
  54. return self.status
  55. def strfunction(self, target, source, env):
  56. import os
  57. comstr = self.commandstr
  58. if env.subst(comstr, target=target, source=source) == "":
  59. comstr = self.command
  60. s = env.subst(comstr, target=target, source=source)
  61. return s
  62. #############################################################################
  63. #############################################################################
  64. def _update_pot_file(target, source, env):
  65. """ Action function for `POTUpdate` builder """
  66. import re
  67. import os
  68. import SCons.Action
  69. nop = lambda target, source, env: 0
  70. # Save scons cwd and os cwd (NOTE: they may be different. After the job, we
  71. # revert each one to its original state).
  72. save_cwd = env.fs.getcwd()
  73. save_os_cwd = os.getcwd()
  74. chdir = target[0].dir
  75. chdir_str = repr(chdir.get_abspath())
  76. # Print chdir message (employ SCons.Action.Action for that. It knows better
  77. # than me how to to this correctly).
  78. env.Execute(SCons.Action.Action(nop, "Entering " + chdir_str))
  79. # Go to target's directory and do our job
  80. env.fs.chdir(chdir, 1) # Go into target's directory
  81. try:
  82. cmd = _CmdRunner('$XGETTEXTCOM', '$XGETTEXTCOMSTR')
  83. action = SCons.Action.Action(cmd, strfunction=cmd.strfunction)
  84. status = action([target[0]], source, env)
  85. except:
  86. # Something went wrong.
  87. env.Execute(SCons.Action.Action(nop, "Leaving " + chdir_str))
  88. # Revert working dirs to previous state and re-throw exception.
  89. env.fs.chdir(save_cwd, 0)
  90. os.chdir(save_os_cwd)
  91. raise
  92. # Print chdir message.
  93. env.Execute(SCons.Action.Action(nop, "Leaving " + chdir_str))
  94. # Revert working dirs to previous state.
  95. env.fs.chdir(save_cwd, 0)
  96. os.chdir(save_os_cwd)
  97. # If the command was not successfull, return error code.
  98. if status: return status
  99. new_content = cmd.out
  100. if not new_content:
  101. # When xgettext finds no internationalized messages, no *.pot is created
  102. # (because we don't want to bother translators with empty POT files).
  103. needs_update = False
  104. explain = "no internationalized messages encountered"
  105. else:
  106. if target[0].exists():
  107. # If the file already exists, it's left unaltered unless its messages
  108. # are outdated (w.r.t. to these recovered by xgettext from sources).
  109. old_content = target[0].get_text_contents()
  110. re_cdate = re.compile(r'^"POT-Creation-Date: .*"$[\r\n]?', re.M)
  111. old_content_nocdate = re.sub(re_cdate, "", old_content)
  112. new_content_nocdate = re.sub(re_cdate, "", new_content)
  113. if (old_content_nocdate == new_content_nocdate):
  114. # Messages are up-to-date
  115. needs_update = False
  116. explain = "messages in file found to be up-to-date"
  117. else:
  118. # Messages are outdated
  119. needs_update = True
  120. explain = "messages in file were outdated"
  121. else:
  122. # No POT file found, create new one
  123. needs_update = True
  124. explain = "new file"
  125. if needs_update:
  126. # Print message employing SCons.Action.Action for that.
  127. msg = "Writing " + repr(str(target[0])) + " (" + explain + ")"
  128. env.Execute(SCons.Action.Action(nop, msg))
  129. f = open(str(target[0]), "w")
  130. f.write(new_content)
  131. f.close()
  132. return 0
  133. else:
  134. # Print message employing SCons.Action.Action for that.
  135. msg = "Not writing " + repr(str(target[0])) + " (" + explain + ")"
  136. env.Execute(SCons.Action.Action(nop, msg))
  137. return 0
  138. #############################################################################
  139. #############################################################################
  140. from SCons.Builder import BuilderBase
  141. #############################################################################
  142. class _POTBuilder(BuilderBase):
  143. def _execute(self, env, target, source, *args):
  144. if not target:
  145. if 'POTDOMAIN' in env and env['POTDOMAIN']:
  146. domain = env['POTDOMAIN']
  147. else:
  148. domain = 'messages'
  149. target = [domain]
  150. return BuilderBase._execute(self, env, target, source, *args)
  151. #############################################################################
  152. #############################################################################
  153. def _scan_xgettext_from_files(target, source, env, files=None, path=None):
  154. """ Parses `POTFILES.in`-like file and returns list of extracted file names.
  155. """
  156. import re
  157. import SCons.Util
  158. import SCons.Node.FS
  159. if files is None:
  160. return 0
  161. if not SCons.Util.is_List(files):
  162. files = [files]
  163. if path is None:
  164. if 'XGETTEXTPATH' in env:
  165. path = env['XGETTEXTPATH']
  166. else:
  167. path = []
  168. if not SCons.Util.is_List(path):
  169. path = [path]
  170. path = SCons.Util.flatten(path)
  171. dirs = ()
  172. for p in path:
  173. if not isinstance(p, SCons.Node.FS.Base):
  174. if SCons.Util.is_String(p):
  175. p = env.subst(p, source=source, target=target)
  176. p = env.arg2nodes(p, env.fs.Dir)
  177. dirs += tuple(p)
  178. # cwd is the default search path (when no path is defined by user)
  179. if not dirs:
  180. dirs = (env.fs.getcwd(),)
  181. # Parse 'POTFILE.in' files.
  182. re_comment = re.compile(r'^#[^\n\r]*$\r?\n?', re.M)
  183. re_emptyln = re.compile(r'^[ \t\r]*$\r?\n?', re.M)
  184. re_trailws = re.compile(r'[ \t\r]+$')
  185. for f in files:
  186. # Find files in search path $XGETTEXTPATH
  187. if isinstance(f, SCons.Node.FS.Base) and f.rexists():
  188. contents = f.get_text_contents()
  189. contents = re_comment.sub("", contents)
  190. contents = re_emptyln.sub("", contents)
  191. contents = re_trailws.sub("", contents)
  192. depnames = contents.splitlines()
  193. for depname in depnames:
  194. depfile = SCons.Node.FS.find_file(depname, dirs)
  195. if not depfile:
  196. depfile = env.arg2nodes(depname, dirs[0].File)
  197. env.Depends(target, depfile)
  198. return 0
  199. #############################################################################
  200. #############################################################################
  201. def _pot_update_emitter(target, source, env):
  202. """ Emitter function for `POTUpdate` builder """
  203. from SCons.Tool.GettextCommon import _POTargetFactory
  204. import SCons.Util
  205. import SCons.Node.FS
  206. if 'XGETTEXTFROM' in env:
  207. xfrom = env['XGETTEXTFROM']
  208. else:
  209. return target, source
  210. if not SCons.Util.is_List(xfrom):
  211. xfrom = [xfrom]
  212. xfrom = SCons.Util.flatten(xfrom)
  213. # Prepare list of 'POTFILE.in' files.
  214. files = []
  215. for xf in xfrom:
  216. if not isinstance(xf, SCons.Node.FS.Base):
  217. if SCons.Util.is_String(xf):
  218. # Interpolate variables in strings
  219. xf = env.subst(xf, source=source, target=target)
  220. xf = env.arg2nodes(xf)
  221. files.extend(xf)
  222. if files:
  223. env.Depends(target, files)
  224. _scan_xgettext_from_files(target, source, env, files)
  225. return target, source
  226. #############################################################################
  227. #############################################################################
  228. from SCons.Environment import _null
  229. #############################################################################
  230. def _POTUpdateBuilderWrapper(env, target=None, source=_null, **kw):
  231. return env._POTUpdateBuilder(target, source, **kw)
  232. #############################################################################
  233. #############################################################################
  234. def _POTUpdateBuilder(env, **kw):
  235. """ Creates `POTUpdate` builder object """
  236. import SCons.Action
  237. from SCons.Tool.GettextCommon import _POTargetFactory
  238. kw['action'] = SCons.Action.Action(_update_pot_file, None)
  239. kw['suffix'] = '$POTSUFFIX'
  240. kw['target_factory'] = _POTargetFactory(env, alias='$POTUPDATE_ALIAS').File
  241. kw['emitter'] = _pot_update_emitter
  242. return _POTBuilder(**kw)
  243. #############################################################################
  244. #############################################################################
  245. def generate(env, **kw):
  246. """ Generate `xgettext` tool """
  247. import SCons.Util
  248. from SCons.Tool.GettextCommon import RPaths, _detect_xgettext
  249. try:
  250. env['XGETTEXT'] = _detect_xgettext(env)
  251. except:
  252. env['XGETTEXT'] = 'xgettext'
  253. # NOTE: sources="$SOURCES" would work as well. However, we use following
  254. # construction to convert absolute paths provided by scons onto paths
  255. # relative to current working dir. Note, that scons expands $SOURCE(S) to
  256. # absolute paths for sources $SOURCE(s) outside of current subtree (e.g. in
  257. # "../"). With source=$SOURCE these absolute paths would be written to the
  258. # resultant *.pot file (and its derived *.po files) as references to lines in
  259. # source code (e.g. referring lines in *.c files). Such references would be
  260. # correct (e.g. in poedit) only on machine on which *.pot was generated and
  261. # would be of no use on other hosts (having a copy of source code located
  262. # in different place in filesystem).
  263. sources = '$( ${_concat( "", SOURCES, "", __env__, XgettextRPaths, TARGET' \
  264. + ', SOURCES)} $)'
  265. # NOTE: the output from $XGETTEXTCOM command must go to stdout, not to a file.
  266. # This is required by the POTUpdate builder's action.
  267. xgettextcom = '$XGETTEXT $XGETTEXTFLAGS $_XGETTEXTPATHFLAGS' \
  268. + ' $_XGETTEXTFROMFLAGS -o - ' + sources
  269. xgettextpathflags = '$( ${_concat( XGETTEXTPATHPREFIX, XGETTEXTPATH' \
  270. + ', XGETTEXTPATHSUFFIX, __env__, RDirs, TARGET, SOURCES)} $)'
  271. xgettextfromflags = '$( ${_concat( XGETTEXTFROMPREFIX, XGETTEXTFROM' \
  272. + ', XGETTEXTFROMSUFFIX, __env__, target=TARGET, source=SOURCES)} $)'
  273. env.SetDefault(
  274. _XGETTEXTDOMAIN='${TARGET.filebase}',
  275. XGETTEXTFLAGS=[],
  276. XGETTEXTCOM=xgettextcom,
  277. XGETTEXTCOMSTR='',
  278. XGETTEXTPATH=[],
  279. XGETTEXTPATHPREFIX='-D',
  280. XGETTEXTPATHSUFFIX='',
  281. XGETTEXTFROM=None,
  282. XGETTEXTFROMPREFIX='-f',
  283. XGETTEXTFROMSUFFIX='',
  284. _XGETTEXTPATHFLAGS=xgettextpathflags,
  285. _XGETTEXTFROMFLAGS=xgettextfromflags,
  286. POTSUFFIX=['.pot'],
  287. POTUPDATE_ALIAS='pot-update',
  288. XgettextRPaths=RPaths(env)
  289. )
  290. env.Append(BUILDERS={
  291. '_POTUpdateBuilder': _POTUpdateBuilder(env)
  292. })
  293. env.AddMethod(_POTUpdateBuilderWrapper, 'POTUpdate')
  294. env.AlwaysBuild(env.Alias('$POTUPDATE_ALIAS'))
  295. #############################################################################
  296. #############################################################################
  297. def exists(env):
  298. """ Check, whether the tool exists """
  299. from SCons.Tool.GettextCommon import _xgettext_exists
  300. try:
  301. return _xgettext_exists(env)
  302. except:
  303. return False
  304. #############################################################################
  305. # Local Variables:
  306. # tab-width:4
  307. # indent-tabs-mode:nil
  308. # End:
  309. # vim: set expandtab tabstop=4 shiftwidth=4: