429 lines
15 KiB
Python
Vendored
429 lines
15 KiB
Python
Vendored
# MIT License
|
|
#
|
|
# Copyright The SCons Foundation
|
|
#
|
|
# Permission is hereby granted, free of charge, to any person obtaining
|
|
# a copy of this software and associated documentation files (the
|
|
# "Software"), to deal in the Software without restriction, including
|
|
# without limitation the rights to use, copy, modify, merge, publish,
|
|
# distribute, sublicense, and/or sell copies of the Software, and to
|
|
# permit persons to whom the Software is furnished to do so, subject to
|
|
# the following conditions:
|
|
#
|
|
# The above copyright notice and this permission notice shall be included
|
|
# in all copies or substantial portions of the Software.
|
|
#
|
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
|
|
# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
|
|
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
|
|
"""Common routines for gettext tools
|
|
|
|
Used by several tools of `gettext` toolset.
|
|
"""
|
|
|
|
import os
|
|
import re
|
|
|
|
import SCons.Util
|
|
import SCons.Warnings
|
|
|
|
class XgettextToolWarning(SCons.Warnings.SConsWarning):
|
|
pass
|
|
|
|
|
|
class XgettextNotFound(XgettextToolWarning):
|
|
pass
|
|
|
|
|
|
class MsginitToolWarning(SCons.Warnings.SConsWarning):
|
|
pass
|
|
|
|
|
|
class MsginitNotFound(MsginitToolWarning):
|
|
pass
|
|
|
|
|
|
class MsgmergeToolWarning(SCons.Warnings.SConsWarning):
|
|
pass
|
|
|
|
|
|
class MsgmergeNotFound(MsgmergeToolWarning):
|
|
pass
|
|
|
|
|
|
class MsgfmtToolWarning(SCons.Warnings.SConsWarning):
|
|
pass
|
|
|
|
|
|
class MsgfmtNotFound(MsgfmtToolWarning):
|
|
pass
|
|
|
|
|
|
SCons.Warnings.enableWarningClass(XgettextToolWarning)
|
|
SCons.Warnings.enableWarningClass(XgettextNotFound)
|
|
SCons.Warnings.enableWarningClass(MsginitToolWarning)
|
|
SCons.Warnings.enableWarningClass(MsginitNotFound)
|
|
SCons.Warnings.enableWarningClass(MsgmergeToolWarning)
|
|
SCons.Warnings.enableWarningClass(MsgmergeNotFound)
|
|
SCons.Warnings.enableWarningClass(MsgfmtToolWarning)
|
|
SCons.Warnings.enableWarningClass(MsgfmtNotFound)
|
|
|
|
|
|
class _POTargetFactory:
|
|
""" A factory of `PO` target files.
|
|
|
|
Factory defaults differ from these of `SCons.Node.FS.FS`. We set `precious`
|
|
(this is required by builders and actions gettext) and `noclean` flags by
|
|
default for all produced nodes.
|
|
"""
|
|
|
|
def __init__(self, env, nodefault: bool=True, alias=None, precious: bool=True
|
|
, noclean: bool=True) -> None:
|
|
""" Object constructor.
|
|
|
|
**Arguments**
|
|
|
|
- *env* (`SCons.Environment.Environment`)
|
|
- *nodefault* (`boolean`) - if `True`, produced nodes will be ignored
|
|
from default target `'.'`
|
|
- *alias* (`string`) - if provided, produced nodes will be automatically
|
|
added to this alias, and alias will be set as `AlwaysBuild`
|
|
- *precious* (`boolean`) - if `True`, the produced nodes will be set as
|
|
`Precious`.
|
|
- *noclen* (`boolean`) - if `True`, the produced nodes will be excluded
|
|
from `Clean`.
|
|
"""
|
|
self.env = env
|
|
self.alias = alias
|
|
self.precious = precious
|
|
self.noclean = noclean
|
|
self.nodefault = nodefault
|
|
|
|
def _create_node(self, name, factory, directory=None, create: int=1):
|
|
""" Create node, and set it up to factory settings. """
|
|
node = factory(name, directory, create)
|
|
node.set_noclean(self.noclean)
|
|
node.set_precious(self.precious)
|
|
if self.nodefault:
|
|
self.env.Ignore('.', node)
|
|
if self.alias:
|
|
self.env.AlwaysBuild(self.env.Alias(self.alias, node))
|
|
return node
|
|
|
|
def Entry(self, name, directory=None, create: int=1):
|
|
""" Create `SCons.Node.FS.Entry` """
|
|
return self._create_node(name, self.env.fs.Entry, directory, create)
|
|
|
|
def File(self, name, directory=None, create: int=1):
|
|
""" Create `SCons.Node.FS.File` """
|
|
return self._create_node(name, self.env.fs.File, directory, create)
|
|
|
|
|
|
_re_comment = re.compile(r'(#[^\n\r]+)$', re.M)
|
|
_re_lang = re.compile(r'([a-zA-Z0-9_]+)', re.M)
|
|
|
|
|
|
def _read_linguas_from_files(env, linguas_files=None):
|
|
""" Parse `LINGUAS` file and return list of extracted languages """
|
|
global _re_comment
|
|
global _re_lang
|
|
if not SCons.Util.is_List(linguas_files) \
|
|
and not SCons.Util.is_String(linguas_files) \
|
|
and not isinstance(linguas_files, SCons.Node.FS.Base) \
|
|
and linguas_files:
|
|
# If, linguas_files==True or such, then read 'LINGUAS' file.
|
|
linguas_files = ['LINGUAS']
|
|
if linguas_files is None:
|
|
return []
|
|
fnodes = env.arg2nodes(linguas_files)
|
|
linguas = []
|
|
for fnode in fnodes:
|
|
contents = _re_comment.sub("", fnode.get_text_contents())
|
|
ls = [l for l in _re_lang.findall(contents) if l]
|
|
linguas.extend(ls)
|
|
return linguas
|
|
|
|
|
|
from SCons.Builder import BuilderBase
|
|
|
|
|
|
class _POFileBuilder(BuilderBase):
|
|
""" `PO` file builder.
|
|
|
|
This is multi-target single-source builder. In typical situation the source
|
|
is single `POT` file, e.g. `messages.pot`, and there are multiple `PO`
|
|
targets to be updated from this `POT`. We must run
|
|
`SCons.Builder.BuilderBase._execute()` separatelly for each target to track
|
|
dependencies separatelly for each target file.
|
|
|
|
**NOTE**: if we call `SCons.Builder.BuilderBase._execute(.., target, ...)`
|
|
with target being list of all targets, all targets would be rebuilt each time
|
|
one of the targets from this list is missing. This would happen, for example,
|
|
when new language `ll` enters `LINGUAS_FILE` (at this moment there is no
|
|
`ll.po` file yet). To avoid this, we override
|
|
`SCons.Builder.BuilerBase._execute()` and call it separatelly for each
|
|
target. Here we also append to the target list the languages read from
|
|
`LINGUAS_FILE`.
|
|
"""
|
|
|
|
#
|
|
# * The argument for overriding _execute(): We must use environment with
|
|
# builder overrides applied (see BuilderBase.__init__(). Here it comes for
|
|
# free.
|
|
# * The argument against using 'emitter': The emitter is called too late
|
|
# by BuilderBase._execute(). If user calls, for example:
|
|
#
|
|
# env.POUpdate(LINGUAS_FILE = 'LINGUAS')
|
|
#
|
|
# the builder throws error, because it is called with target=None,
|
|
# source=None and is trying to "generate" sources or target list first.
|
|
# If user calls
|
|
#
|
|
# env.POUpdate(['foo', 'baz'], LINGUAS_FILE = 'LINGUAS')
|
|
#
|
|
# the env.BuilderWrapper() calls our builder with target=None,
|
|
# source=['foo', 'baz']. The BuilderBase._execute() then splits execution
|
|
# and execute iterativelly (recursion) self._execute(None, source[i]).
|
|
# After that it calls emitter (which is quite too late). The emitter is
|
|
# also called in each iteration, what makes things yet worse.
|
|
def __init__(self, env, **kw) -> None:
|
|
if 'suffix' not in kw:
|
|
kw['suffix'] = '$POSUFFIX'
|
|
if 'src_suffix' not in kw:
|
|
kw['src_suffix'] = '$POTSUFFIX'
|
|
if 'src_builder' not in kw:
|
|
kw['src_builder'] = '_POTUpdateBuilder'
|
|
if 'single_source' not in kw:
|
|
kw['single_source'] = True
|
|
alias = None
|
|
if 'target_alias' in kw:
|
|
alias = kw['target_alias']
|
|
del kw['target_alias']
|
|
if 'target_factory' not in kw:
|
|
kw['target_factory'] = _POTargetFactory(env, alias=alias).File
|
|
super().__init__(**kw)
|
|
|
|
def _execute(self, env, target, source, *args, **kw):
|
|
""" Execute builder's actions.
|
|
|
|
Here we append to `target` the languages read from `$LINGUAS_FILE` and
|
|
apply `SCons.Builder.BuilderBase._execute()` separatelly to each target.
|
|
The arguments and return value are same as for
|
|
`SCons.Builder.BuilderBase._execute()`.
|
|
"""
|
|
import SCons.Node
|
|
linguas_files = None
|
|
if 'LINGUAS_FILE' in env and env['LINGUAS_FILE']:
|
|
linguas_files = env['LINGUAS_FILE']
|
|
# This prevents endless recursion loop (we'll be invoked once for
|
|
# each target appended here, we must not extend the list again).
|
|
env['LINGUAS_FILE'] = None
|
|
linguas = _read_linguas_from_files(env, linguas_files)
|
|
if SCons.Util.is_List(target):
|
|
target.extend(linguas)
|
|
elif target is not None:
|
|
target = [target] + linguas
|
|
else:
|
|
target = linguas
|
|
if not target:
|
|
# Let the SCons.BuilderBase to handle this patologic situation
|
|
return BuilderBase._execute(self, env, target, source, *args, **kw)
|
|
# The rest is ours
|
|
if not SCons.Util.is_List(target):
|
|
target = [target]
|
|
result = []
|
|
for tgt in target:
|
|
r = BuilderBase._execute(self, env, [tgt], source, *args, **kw)
|
|
result.extend(r)
|
|
if linguas_files is not None:
|
|
env['LINGUAS_FILE'] = linguas_files
|
|
return SCons.Node.NodeList(result)
|
|
|
|
|
|
def _translate(env, target=None, source=SCons.Environment._null, *args, **kw):
|
|
""" Function for `Translate()` pseudo-builder """
|
|
if target is None: target = []
|
|
pot = env.POTUpdate(None, source, *args, **kw)
|
|
po = env.POUpdate(target, pot, *args, **kw)
|
|
return po
|
|
|
|
|
|
class RPaths:
|
|
""" Callable object, which returns pathnames relative to SCons current
|
|
working directory.
|
|
|
|
It seems like `SCons.Node.FS.Base.get_path()` returns absolute paths
|
|
for nodes that are outside of current working directory (`env.fs.getcwd()`).
|
|
Here, we often have `SConscript`, `POT` and `PO` files within `po/`
|
|
directory and source files (e.g. `*.c`) outside of it. When generating `POT`
|
|
template file, references to source files are written to `POT` template, so
|
|
a translator may later quickly jump to appropriate source file and line from
|
|
its `PO` editor (e.g. `poedit`). Relative paths in `PO` file are usually
|
|
interpreted by `PO` editor as paths relative to the place, where `PO` file
|
|
lives. The absolute paths would make resultant `POT` file nonportable, as
|
|
the references would be correct only on the machine, where `POT` file was
|
|
recently re-created. For such reason, we need a function, which always
|
|
returns relative paths. This is the purpose of `RPaths` callable object.
|
|
|
|
The `__call__` method returns paths relative to current working directory, but
|
|
we assume, that *xgettext(1)* is run from the directory, where target file is
|
|
going to be created.
|
|
|
|
Note, that this may not work for files distributed over several hosts or
|
|
across different drives on windows. We assume here, that single local
|
|
filesystem holds both source files and target `POT` templates.
|
|
|
|
Intended use of `RPaths` - in `xgettext.py`::
|
|
|
|
def generate(env):
|
|
from GettextCommon import RPaths
|
|
...
|
|
sources = '$( ${_concat( "", SOURCES, "", __env__, XgettextRPaths, TARGET, SOURCES)} $)'
|
|
env.Append(
|
|
...
|
|
XGETTEXTCOM = 'XGETTEXT ... ' + sources,
|
|
...
|
|
XgettextRPaths = RPaths(env)
|
|
)
|
|
"""
|
|
|
|
# NOTE: This callable object returns pathnames of dirs/files relative to
|
|
# current working directory. The pathname remains relative also for entries
|
|
# that are outside of current working directory (node, that
|
|
# SCons.Node.FS.File and siblings return absolute path in such case). For
|
|
# simplicity we compute path relative to current working directory, this
|
|
# seems be enough for our purposes (don't need TARGET variable and
|
|
# SCons.Defaults.Variable_Caller stuff).
|
|
|
|
def __init__(self, env) -> None:
|
|
""" Initialize `RPaths` callable object.
|
|
|
|
**Arguments**:
|
|
|
|
- *env* - a `SCons.Environment.Environment` object, defines *current
|
|
working dir*.
|
|
"""
|
|
self.env = env
|
|
|
|
# FIXME: I'm not sure, how it should be implemented (what the *args are in
|
|
# general, what is **kw).
|
|
def __call__(self, nodes, *args, **kw):
|
|
""" Return nodes' paths (strings) relative to current working directory.
|
|
|
|
**Arguments**:
|
|
|
|
- *nodes* ([`SCons.Node.FS.Base`]) - list of nodes.
|
|
- *args* - currently unused.
|
|
- *kw* - currently unused.
|
|
|
|
**Returns**:
|
|
|
|
- Tuple of strings, which represent paths relative to current working
|
|
directory (for given environment).
|
|
"""
|
|
import SCons.Node.FS
|
|
rpaths = ()
|
|
cwd = self.env.fs.getcwd().get_abspath()
|
|
for node in nodes:
|
|
rpath = None
|
|
if isinstance(node, SCons.Node.FS.Base):
|
|
rpath = os.path.relpath(node.get_abspath(), cwd)
|
|
# FIXME: Other types possible here?
|
|
if rpath is not None:
|
|
rpaths += (rpath,)
|
|
return rpaths
|
|
|
|
|
|
def _init_po_files(target, source, env):
|
|
""" Action function for `POInit` builder. """
|
|
nop = lambda target, source, env: 0
|
|
if 'POAUTOINIT' in env:
|
|
autoinit = env['POAUTOINIT']
|
|
else:
|
|
autoinit = False
|
|
# Well, if everything outside works well, this loop should do single
|
|
# iteration. Otherwise we are rebuilding all the targets even, if just
|
|
# one has changed (but is this our fault?).
|
|
for tgt in target:
|
|
if not tgt.exists():
|
|
if autoinit:
|
|
action = SCons.Action.Action('$MSGINITCOM', '$MSGINITCOMSTR')
|
|
else:
|
|
msg = 'File ' + repr(str(tgt)) + ' does not exist. ' \
|
|
+ 'If you are a translator, you can create it through: \n' \
|
|
+ '$MSGINITCOM'
|
|
action = SCons.Action.Action(nop, msg)
|
|
status = action([tgt], source, env)
|
|
if status: return status
|
|
return 0
|
|
|
|
|
|
def _detect_xgettext(env):
|
|
""" Detects *xgettext(1)* binary """
|
|
if 'XGETTEXT' in env:
|
|
return env['XGETTEXT']
|
|
xgettext = env.Detect('xgettext')
|
|
if xgettext:
|
|
return xgettext
|
|
raise SCons.Errors.StopError(XgettextNotFound, "Could not detect xgettext")
|
|
return None
|
|
|
|
|
|
def _xgettext_exists(env):
|
|
return _detect_xgettext(env)
|
|
|
|
|
|
def _detect_msginit(env):
|
|
""" Detects *msginit(1)* program. """
|
|
if 'MSGINIT' in env:
|
|
return env['MSGINIT']
|
|
msginit = env.Detect('msginit')
|
|
if msginit:
|
|
return msginit
|
|
raise SCons.Errors.StopError(MsginitNotFound, "Could not detect msginit")
|
|
return None
|
|
|
|
|
|
def _msginit_exists(env):
|
|
return _detect_msginit(env)
|
|
|
|
|
|
def _detect_msgmerge(env):
|
|
""" Detects *msgmerge(1)* program. """
|
|
if 'MSGMERGE' in env:
|
|
return env['MSGMERGE']
|
|
msgmerge = env.Detect('msgmerge')
|
|
if msgmerge:
|
|
return msgmerge
|
|
raise SCons.Errors.StopError(MsgmergeNotFound, "Could not detect msgmerge")
|
|
return None
|
|
|
|
|
|
def _msgmerge_exists(env):
|
|
return _detect_msgmerge(env)
|
|
|
|
|
|
def _detect_msgfmt(env):
|
|
""" Detects *msgmfmt(1)* program. """
|
|
if 'MSGFMT' in env:
|
|
return env['MSGFMT']
|
|
msgfmt = env.Detect('msgfmt')
|
|
if msgfmt:
|
|
return msgfmt
|
|
raise SCons.Errors.StopError(MsgfmtNotFound, "Could not detect msgfmt")
|
|
return None
|
|
|
|
|
|
def _msgfmt_exists(env):
|
|
return _detect_msgfmt(env)
|
|
|
|
|
|
def tool_list(platform, env):
|
|
""" List tools that shall be generated by top-level `gettext` tool """
|
|
return ['xgettext', 'msginit', 'msgmerge', 'msgfmt']
|
|
|