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.

469 lines
18 KiB

6 years ago
  1. """SCons.Tool.GettextCommon module
  2. Used by several tools of `gettext` toolset.
  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/GettextCommon.py rel_3.0.0:4395:8972f6a2f699 2017/09/18 12:59:24 bdbaddog"
  25. import SCons.Warnings
  26. import re
  27. #############################################################################
  28. class XgettextToolWarning(SCons.Warnings.Warning): pass
  29. class XgettextNotFound(XgettextToolWarning): pass
  30. class MsginitToolWarning(SCons.Warnings.Warning): pass
  31. class MsginitNotFound(MsginitToolWarning): pass
  32. class MsgmergeToolWarning(SCons.Warnings.Warning): pass
  33. class MsgmergeNotFound(MsgmergeToolWarning): pass
  34. class MsgfmtToolWarning(SCons.Warnings.Warning): pass
  35. class MsgfmtNotFound(MsgfmtToolWarning): pass
  36. #############################################################################
  37. SCons.Warnings.enableWarningClass(XgettextToolWarning)
  38. SCons.Warnings.enableWarningClass(XgettextNotFound)
  39. SCons.Warnings.enableWarningClass(MsginitToolWarning)
  40. SCons.Warnings.enableWarningClass(MsginitNotFound)
  41. SCons.Warnings.enableWarningClass(MsgmergeToolWarning)
  42. SCons.Warnings.enableWarningClass(MsgmergeNotFound)
  43. SCons.Warnings.enableWarningClass(MsgfmtToolWarning)
  44. SCons.Warnings.enableWarningClass(MsgfmtNotFound)
  45. #############################################################################
  46. #############################################################################
  47. class _POTargetFactory(object):
  48. """ A factory of `PO` target files.
  49. Factory defaults differ from these of `SCons.Node.FS.FS`. We set `precious`
  50. (this is required by builders and actions gettext) and `noclean` flags by
  51. default for all produced nodes.
  52. """
  53. def __init__(self, env, nodefault=True, alias=None, precious=True
  54. , noclean=True):
  55. """ Object constructor.
  56. **Arguments**
  57. - *env* (`SCons.Environment.Environment`)
  58. - *nodefault* (`boolean`) - if `True`, produced nodes will be ignored
  59. from default target `'.'`
  60. - *alias* (`string`) - if provided, produced nodes will be automatically
  61. added to this alias, and alias will be set as `AlwaysBuild`
  62. - *precious* (`boolean`) - if `True`, the produced nodes will be set as
  63. `Precious`.
  64. - *noclen* (`boolean`) - if `True`, the produced nodes will be excluded
  65. from `Clean`.
  66. """
  67. self.env = env
  68. self.alias = alias
  69. self.precious = precious
  70. self.noclean = noclean
  71. self.nodefault = nodefault
  72. def _create_node(self, name, factory, directory=None, create=1):
  73. """ Create node, and set it up to factory settings. """
  74. import SCons.Util
  75. node = factory(name, directory, create)
  76. node.set_noclean(self.noclean)
  77. node.set_precious(self.precious)
  78. if self.nodefault:
  79. self.env.Ignore('.', node)
  80. if self.alias:
  81. self.env.AlwaysBuild(self.env.Alias(self.alias, node))
  82. return node
  83. def Entry(self, name, directory=None, create=1):
  84. """ Create `SCons.Node.FS.Entry` """
  85. return self._create_node(name, self.env.fs.Entry, directory, create)
  86. def File(self, name, directory=None, create=1):
  87. """ Create `SCons.Node.FS.File` """
  88. return self._create_node(name, self.env.fs.File, directory, create)
  89. #############################################################################
  90. #############################################################################
  91. _re_comment = re.compile(r'(#[^\n\r]+)$', re.M)
  92. _re_lang = re.compile(r'([a-zA-Z0-9_]+)', re.M)
  93. #############################################################################
  94. def _read_linguas_from_files(env, linguas_files=None):
  95. """ Parse `LINGUAS` file and return list of extracted languages """
  96. import SCons.Util
  97. import SCons.Environment
  98. global _re_comment
  99. global _re_lang
  100. if not SCons.Util.is_List(linguas_files) \
  101. and not SCons.Util.is_String(linguas_files) \
  102. and not isinstance(linguas_files, SCons.Node.FS.Base) \
  103. and linguas_files:
  104. # If, linguas_files==True or such, then read 'LINGUAS' file.
  105. linguas_files = ['LINGUAS']
  106. if linguas_files is None:
  107. return []
  108. fnodes = env.arg2nodes(linguas_files)
  109. linguas = []
  110. for fnode in fnodes:
  111. contents = _re_comment.sub("", fnode.get_text_contents())
  112. ls = [l for l in _re_lang.findall(contents) if l]
  113. linguas.extend(ls)
  114. return linguas
  115. #############################################################################
  116. #############################################################################
  117. from SCons.Builder import BuilderBase
  118. #############################################################################
  119. class _POFileBuilder(BuilderBase):
  120. """ `PO` file builder.
  121. This is multi-target single-source builder. In typical situation the source
  122. is single `POT` file, e.g. `messages.pot`, and there are multiple `PO`
  123. targets to be updated from this `POT`. We must run
  124. `SCons.Builder.BuilderBase._execute()` separatelly for each target to track
  125. dependencies separatelly for each target file.
  126. **NOTE**: if we call `SCons.Builder.BuilderBase._execute(.., target, ...)`
  127. with target being list of all targets, all targets would be rebuilt each time
  128. one of the targets from this list is missing. This would happen, for example,
  129. when new language `ll` enters `LINGUAS_FILE` (at this moment there is no
  130. `ll.po` file yet). To avoid this, we override
  131. `SCons.Builder.BuilerBase._execute()` and call it separatelly for each
  132. target. Here we also append to the target list the languages read from
  133. `LINGUAS_FILE`.
  134. """
  135. #
  136. # * The argument for overriding _execute(): We must use environment with
  137. # builder overrides applied (see BuilderBase.__init__(). Here it comes for
  138. # free.
  139. # * The argument against using 'emitter': The emitter is called too late
  140. # by BuilderBase._execute(). If user calls, for example:
  141. #
  142. # env.POUpdate(LINGUAS_FILE = 'LINGUAS')
  143. #
  144. # the builder throws error, because it is called with target=None,
  145. # source=None and is trying to "generate" sources or target list first.
  146. # If user calls
  147. #
  148. # env.POUpdate(['foo', 'baz'], LINGUAS_FILE = 'LINGUAS')
  149. #
  150. # the env.BuilderWrapper() calls our builder with target=None,
  151. # source=['foo', 'baz']. The BuilderBase._execute() then splits execution
  152. # and execute iterativelly (recursion) self._execute(None, source[i]).
  153. # After that it calls emitter (which is quite too late). The emitter is
  154. # also called in each iteration, what makes things yet worse.
  155. def __init__(self, env, **kw):
  156. if not 'suffix' in kw:
  157. kw['suffix'] = '$POSUFFIX'
  158. if not 'src_suffix' in kw:
  159. kw['src_suffix'] = '$POTSUFFIX'
  160. if not 'src_builder' in kw:
  161. kw['src_builder'] = '_POTUpdateBuilder'
  162. if not 'single_source' in kw:
  163. kw['single_source'] = True
  164. alias = None
  165. if 'target_alias' in kw:
  166. alias = kw['target_alias']
  167. del kw['target_alias']
  168. if not 'target_factory' in kw:
  169. kw['target_factory'] = _POTargetFactory(env, alias=alias).File
  170. BuilderBase.__init__(self, **kw)
  171. def _execute(self, env, target, source, *args, **kw):
  172. """ Execute builder's actions.
  173. Here we append to `target` the languages read from `$LINGUAS_FILE` and
  174. apply `SCons.Builder.BuilderBase._execute()` separatelly to each target.
  175. The arguments and return value are same as for
  176. `SCons.Builder.BuilderBase._execute()`.
  177. """
  178. import SCons.Util
  179. import SCons.Node
  180. linguas_files = None
  181. if 'LINGUAS_FILE' in env and env['LINGUAS_FILE']:
  182. linguas_files = env['LINGUAS_FILE']
  183. # This prevents endless recursion loop (we'll be invoked once for
  184. # each target appended here, we must not extend the list again).
  185. env['LINGUAS_FILE'] = None
  186. linguas = _read_linguas_from_files(env, linguas_files)
  187. if SCons.Util.is_List(target):
  188. target.extend(linguas)
  189. elif target is not None:
  190. target = [target] + linguas
  191. else:
  192. target = linguas
  193. if not target:
  194. # Let the SCons.BuilderBase to handle this patologic situation
  195. return BuilderBase._execute(self, env, target, source, *args, **kw)
  196. # The rest is ours
  197. if not SCons.Util.is_List(target):
  198. target = [target]
  199. result = []
  200. for tgt in target:
  201. r = BuilderBase._execute(self, env, [tgt], source, *args, **kw)
  202. result.extend(r)
  203. if linguas_files is not None:
  204. env['LINGUAS_FILE'] = linguas_files
  205. return SCons.Node.NodeList(result)
  206. #############################################################################
  207. import SCons.Environment
  208. #############################################################################
  209. def _translate(env, target=None, source=SCons.Environment._null, *args, **kw):
  210. """ Function for `Translate()` pseudo-builder """
  211. if target is None: target = []
  212. pot = env.POTUpdate(None, source, *args, **kw)
  213. po = env.POUpdate(target, pot, *args, **kw)
  214. return po
  215. #############################################################################
  216. #############################################################################
  217. class RPaths(object):
  218. """ Callable object, which returns pathnames relative to SCons current
  219. working directory.
  220. It seems like `SCons.Node.FS.Base.get_path()` returns absolute paths
  221. for nodes that are outside of current working directory (`env.fs.getcwd()`).
  222. Here, we often have `SConscript`, `POT` and `PO` files within `po/`
  223. directory and source files (e.g. `*.c`) outside of it. When generating `POT`
  224. template file, references to source files are written to `POT` template, so
  225. a translator may later quickly jump to appropriate source file and line from
  226. its `PO` editor (e.g. `poedit`). Relative paths in `PO` file are usually
  227. interpreted by `PO` editor as paths relative to the place, where `PO` file
  228. lives. The absolute paths would make resultant `POT` file nonportable, as
  229. the references would be correct only on the machine, where `POT` file was
  230. recently re-created. For such reason, we need a function, which always
  231. returns relative paths. This is the purpose of `RPaths` callable object.
  232. The `__call__` method returns paths relative to current working directory, but
  233. we assume, that *xgettext(1)* is run from the directory, where target file is
  234. going to be created.
  235. Note, that this may not work for files distributed over several hosts or
  236. across different drives on windows. We assume here, that single local
  237. filesystem holds both source files and target `POT` templates.
  238. Intended use of `RPaths` - in `xgettext.py`::
  239. def generate(env):
  240. from GettextCommon import RPaths
  241. ...
  242. sources = '$( ${_concat( "", SOURCES, "", __env__, XgettextRPaths, TARGET, SOURCES)} $)'
  243. env.Append(
  244. ...
  245. XGETTEXTCOM = 'XGETTEXT ... ' + sources,
  246. ...
  247. XgettextRPaths = RPaths(env)
  248. )
  249. """
  250. # NOTE: This callable object returns pathnames of dirs/files relative to
  251. # current working directory. The pathname remains relative also for entries
  252. # that are outside of current working directory (node, that
  253. # SCons.Node.FS.File and siblings return absolute path in such case). For
  254. # simplicity we compute path relative to current working directory, this
  255. # seems be enough for our purposes (don't need TARGET variable and
  256. # SCons.Defaults.Variable_Caller stuff).
  257. def __init__(self, env):
  258. """ Initialize `RPaths` callable object.
  259. **Arguments**:
  260. - *env* - a `SCons.Environment.Environment` object, defines *current
  261. working dir*.
  262. """
  263. self.env = env
  264. # FIXME: I'm not sure, how it should be implemented (what the *args are in
  265. # general, what is **kw).
  266. def __call__(self, nodes, *args, **kw):
  267. """ Return nodes' paths (strings) relative to current working directory.
  268. **Arguments**:
  269. - *nodes* ([`SCons.Node.FS.Base`]) - list of nodes.
  270. - *args* - currently unused.
  271. - *kw* - currently unused.
  272. **Returns**:
  273. - Tuple of strings, which represent paths relative to current working
  274. directory (for given environment).
  275. """
  276. import os
  277. import SCons.Node.FS
  278. rpaths = ()
  279. cwd = self.env.fs.getcwd().get_abspath()
  280. for node in nodes:
  281. rpath = None
  282. if isinstance(node, SCons.Node.FS.Base):
  283. rpath = os.path.relpath(node.get_abspath(), cwd)
  284. # FIXME: Other types possible here?
  285. if rpath is not None:
  286. rpaths += (rpath,)
  287. return rpaths
  288. #############################################################################
  289. #############################################################################
  290. def _init_po_files(target, source, env):
  291. """ Action function for `POInit` builder. """
  292. nop = lambda target, source, env: 0
  293. if 'POAUTOINIT' in env:
  294. autoinit = env['POAUTOINIT']
  295. else:
  296. autoinit = False
  297. # Well, if everything outside works well, this loop should do single
  298. # iteration. Otherwise we are rebuilding all the targets even, if just
  299. # one has changed (but is this our fault?).
  300. for tgt in target:
  301. if not tgt.exists():
  302. if autoinit:
  303. action = SCons.Action.Action('$MSGINITCOM', '$MSGINITCOMSTR')
  304. else:
  305. msg = 'File ' + repr(str(tgt)) + ' does not exist. ' \
  306. + 'If you are a translator, you can create it through: \n' \
  307. + '$MSGINITCOM'
  308. action = SCons.Action.Action(nop, msg)
  309. status = action([tgt], source, env)
  310. if status: return status
  311. return 0
  312. #############################################################################
  313. #############################################################################
  314. def _detect_xgettext(env):
  315. """ Detects *xgettext(1)* binary """
  316. if 'XGETTEXT' in env:
  317. return env['XGETTEXT']
  318. xgettext = env.Detect('xgettext');
  319. if xgettext:
  320. return xgettext
  321. raise SCons.Errors.StopError(XgettextNotFound, "Could not detect xgettext")
  322. return None
  323. #############################################################################
  324. def _xgettext_exists(env):
  325. return _detect_xgettext(env)
  326. #############################################################################
  327. #############################################################################
  328. def _detect_msginit(env):
  329. """ Detects *msginit(1)* program. """
  330. if 'MSGINIT' in env:
  331. return env['MSGINIT']
  332. msginit = env.Detect('msginit');
  333. if msginit:
  334. return msginit
  335. raise SCons.Errors.StopError(MsginitNotFound, "Could not detect msginit")
  336. return None
  337. #############################################################################
  338. def _msginit_exists(env):
  339. return _detect_msginit(env)
  340. #############################################################################
  341. #############################################################################
  342. def _detect_msgmerge(env):
  343. """ Detects *msgmerge(1)* program. """
  344. if 'MSGMERGE' in env:
  345. return env['MSGMERGE']
  346. msgmerge = env.Detect('msgmerge');
  347. if msgmerge:
  348. return msgmerge
  349. raise SCons.Errors.StopError(MsgmergeNotFound, "Could not detect msgmerge")
  350. return None
  351. #############################################################################
  352. def _msgmerge_exists(env):
  353. return _detect_msgmerge(env)
  354. #############################################################################
  355. #############################################################################
  356. def _detect_msgfmt(env):
  357. """ Detects *msgmfmt(1)* program. """
  358. if 'MSGFMT' in env:
  359. return env['MSGFMT']
  360. msgfmt = env.Detect('msgfmt');
  361. if msgfmt:
  362. return msgfmt
  363. raise SCons.Errors.StopError(MsgfmtNotFound, "Could not detect msgfmt")
  364. return None
  365. #############################################################################
  366. def _msgfmt_exists(env):
  367. return _detect_msgfmt(env)
  368. #############################################################################
  369. #############################################################################
  370. def tool_list(platform, env):
  371. """ List tools that shall be generated by top-level `gettext` tool """
  372. return ['xgettext', 'msginit', 'msgmerge', 'msgfmt']
  373. #############################################################################