# 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. """Tool specific initialization of `xgettext` tool.""" import os import re import sys from subprocess import PIPE import SCons.Action import SCons.Node.FS import SCons.Tool import SCons.Util import SCons.Warnings from SCons.Builder import BuilderBase from SCons.Environment import _null from SCons.Platform.cygwin import CYGWIN_DEFAULT_PATHS from SCons.Platform.mingw import MINGW_DEFAULT_PATHS from SCons.Tool.GettextCommon import ( _detect_xgettext, _POTargetFactory, RPaths, _xgettext_exists, # XgettextToolWarning, ) class _CmdRunner: """ Callable object, which runs shell command storing its stdout and stderr to variables. It also provides `strfunction()` method, which shall be used by scons Action objects to print command string. """ def __init__(self, command, commandstr=None) -> None: self.out = None self.err = None self.status = None self.command = command self.commandstr = commandstr def __call__(self, target, source, env): kw = { 'stdout': PIPE, 'stderr': PIPE, 'universal_newlines': True, 'shell': True } command = env.subst(self.command, target=target, source=source) cp = SCons.Action.scons_subproc_run(env, command, **kw) self.status = cp.returncode self.out = cp.stdout self.err = cp.stderr if self.err: sys.stderr.write(str(self.err)) return self.status def strfunction(self, target, source, env): comstr = self.commandstr if env.subst(comstr, target=target, source=source) == "": comstr = self.command s = env.subst(comstr, target=target, source=source) return s def _update_pot_file(target, source, env): """ Action function for `POTUpdate` builder """ nop = lambda target, source, env: 0 # Save scons cwd and os cwd (NOTE: they may be different. After the job, we # revert each one to its original state). save_cwd = env.fs.getcwd() save_os_cwd = os.getcwd() chdir = target[0].dir chdir_str = repr(chdir.get_abspath()) # Print chdir message (employ SCons.Action.Action for that. It knows better # than me how to to this correctly). env.Execute(SCons.Action.Action(nop, "Entering " + chdir_str)) # Go to target's directory and do our job env.fs.chdir(chdir, 1) # Go into target's directory try: cmd = _CmdRunner('$XGETTEXTCOM', '$XGETTEXTCOMSTR') action = SCons.Action.Action(cmd, strfunction=cmd.strfunction) status = action([target[0]], source, env) except: # Something went wrong. env.Execute(SCons.Action.Action(nop, "Leaving " + chdir_str)) # Revert working dirs to previous state and re-throw exception. env.fs.chdir(save_cwd, 0) os.chdir(save_os_cwd) raise # Print chdir message. env.Execute(SCons.Action.Action(nop, "Leaving " + chdir_str)) # Revert working dirs to previous state. env.fs.chdir(save_cwd, 0) os.chdir(save_os_cwd) # If the command was not successfull, return error code. if status: return status new_content = cmd.out if not new_content: # When xgettext finds no internationalized messages, no *.pot is created # (because we don't want to bother translators with empty POT files). needs_update = False explain = "no internationalized messages encountered" else: if target[0].exists(): # If the file already exists, it's left unaltered unless its messages # are outdated (w.r.t. to these recovered by xgettext from sources). old_content = target[0].get_text_contents() re_cdate = re.compile(r'^"POT-Creation-Date: .*"$[\r\n]?', re.M) old_content_nocdate = re.sub(re_cdate, "", old_content) new_content_nocdate = re.sub(re_cdate, "", new_content) if old_content_nocdate == new_content_nocdate: # Messages are up-to-date needs_update = False explain = "messages in file found to be up-to-date" else: # Messages are outdated needs_update = True explain = "messages in file were outdated" else: # No POT file found, create new one needs_update = True explain = "new file" if needs_update: # Print message employing SCons.Action.Action for that. msg = "Writing " + repr(str(target[0])) + " (" + explain + ")" env.Execute(SCons.Action.Action(nop, msg)) f = open(str(target[0]), "w") f.write(new_content) f.close() return 0 else: # Print message employing SCons.Action.Action for that. msg = "Not writing " + repr(str(target[0])) + " (" + explain + ")" env.Execute(SCons.Action.Action(nop, msg)) return 0 class _POTBuilder(BuilderBase): def _execute(self, env, target, source, *args): if not target: if 'POTDOMAIN' in env and env['POTDOMAIN']: domain = env['POTDOMAIN'] else: domain = 'messages' target = [domain] return BuilderBase._execute(self, env, target, source, *args) def _scan_xgettext_from_files(target, source, env, files=None, path=None) -> int: """ Parses `POTFILES.in`-like file and returns list of extracted file names. """ if files is None: return 0 if not SCons.Util.is_List(files): files = [files] if path is None: if 'XGETTEXTPATH' in env: path = env['XGETTEXTPATH'] else: path = [] if not SCons.Util.is_List(path): path = [path] path = SCons.Util.flatten(path) dirs = () for p in path: if not isinstance(p, SCons.Node.FS.Base): if SCons.Util.is_String(p): p = env.subst(p, source=source, target=target) p = env.arg2nodes(p, env.fs.Dir) dirs += tuple(p) # cwd is the default search path (when no path is defined by user) if not dirs: dirs = (env.fs.getcwd(),) # Parse 'POTFILE.in' files. re_comment = re.compile(r'^#[^\n\r]*$\r?\n?', re.M) re_emptyln = re.compile(r'^[ \t\r]*$\r?\n?', re.M) re_trailws = re.compile(r'[ \t\r]+$') for f in files: # Find files in search path $XGETTEXTPATH if isinstance(f, SCons.Node.FS.Base) and f.rexists(): contents = f.get_text_contents() contents = re_comment.sub("", contents) contents = re_emptyln.sub("", contents) contents = re_trailws.sub("", contents) depnames = contents.splitlines() for depname in depnames: depfile = SCons.Node.FS.find_file(depname, dirs) if not depfile: depfile = env.arg2nodes(depname, dirs[0].File) env.Depends(target, depfile) return 0 def _pot_update_emitter(target, source, env): """ Emitter function for `POTUpdate` builder """ if 'XGETTEXTFROM' in env: xfrom = env['XGETTEXTFROM'] else: return target, source if not SCons.Util.is_List(xfrom): xfrom = [xfrom] xfrom = SCons.Util.flatten(xfrom) # Prepare list of 'POTFILE.in' files. files = [] for xf in xfrom: if not isinstance(xf, SCons.Node.FS.Base): if SCons.Util.is_String(xf): # Interpolate variables in strings xf = env.subst(xf, source=source, target=target) xf = env.arg2nodes(xf) files.extend(xf) if files: env.Depends(target, files) _scan_xgettext_from_files(target, source, env, files) return target, source def _POTUpdateBuilderWrapper(env, target=None, source=_null, **kw): return env._POTUpdateBuilder(target, source, **kw) def _POTUpdateBuilder(env, **kw): """ Creates `POTUpdate` builder object """ kw['action'] = SCons.Action.Action(_update_pot_file, None) kw['suffix'] = '$POTSUFFIX' kw['target_factory'] = _POTargetFactory(env, alias='$POTUPDATE_ALIAS').File kw['emitter'] = _pot_update_emitter return _POTBuilder(**kw) def generate(env, **kw) -> None: """ Generate `xgettext` tool """ if sys.platform == 'win32': xgettext = SCons.Tool.find_program_path(env, 'xgettext', default_paths=MINGW_DEFAULT_PATHS + CYGWIN_DEFAULT_PATHS ) if xgettext: xgettext_bin_dir = os.path.dirname(xgettext) env.AppendENVPath('PATH', xgettext_bin_dir) else: SCons.Warnings.warn( # XgettextToolWarning, # using this breaks test, so keep: SCons.Warnings.SConsWarning, 'xgettext tool requested, but binary not found in ENV PATH' ) try: env['XGETTEXT'] = _detect_xgettext(env) except: env['XGETTEXT'] = 'xgettext' # NOTE: sources="$SOURCES" would work as well. However, we use following # construction to convert absolute paths provided by scons onto paths # relative to current working dir. Note, that scons expands $SOURCE(S) to # absolute paths for sources $SOURCE(s) outside of current subtree (e.g. in # "../"). With source=$SOURCE these absolute paths would be written to the # resultant *.pot file (and its derived *.po files) as references to lines in # source code (e.g. referring lines in *.c files). Such references would be # correct (e.g. in poedit) only on machine on which *.pot was generated and # would be of no use on other hosts (having a copy of source code located # in different place in filesystem). sources = '$( ${_concat( "", SOURCES, "", __env__, XgettextRPaths, TARGET' \ + ', SOURCES)} $)' # NOTE: the output from $XGETTEXTCOM command must go to stdout, not to a file. # This is required by the POTUpdate builder's action. xgettextcom = '$XGETTEXT $XGETTEXTFLAGS $_XGETTEXTPATHFLAGS' \ + ' $_XGETTEXTFROMFLAGS -o - ' + sources xgettextpathflags = '$( ${_concat( XGETTEXTPATHPREFIX, XGETTEXTPATH' \ + ', XGETTEXTPATHSUFFIX, __env__, RDirs, TARGET, SOURCES)} $)' xgettextfromflags = '$( ${_concat( XGETTEXTFROMPREFIX, XGETTEXTFROM' \ + ', XGETTEXTFROMSUFFIX, __env__, target=TARGET, source=SOURCES)} $)' env.SetDefault( _XGETTEXTDOMAIN='${TARGET.filebase}', XGETTEXTFLAGS=[], XGETTEXTCOM=xgettextcom, XGETTEXTCOMSTR='', XGETTEXTPATH=[], XGETTEXTPATHPREFIX='-D', XGETTEXTPATHSUFFIX='', XGETTEXTFROM=None, XGETTEXTFROMPREFIX='-f', XGETTEXTFROMSUFFIX='', _XGETTEXTPATHFLAGS=xgettextpathflags, _XGETTEXTFROMFLAGS=xgettextfromflags, POTSUFFIX=['.pot'], POTUPDATE_ALIAS='pot-update', XgettextRPaths=RPaths(env) ) env.Append(BUILDERS={ '_POTUpdateBuilder': _POTUpdateBuilder(env) }) env.AddMethod(_POTUpdateBuilderWrapper, 'POTUpdate') env.AlwaysBuild(env.Alias('$POTUPDATE_ALIAS')) def exists(env): """ Check, whether the tool exists """ try: return _xgettext_exists(env) except: return False # Local Variables: # tab-width:4 # indent-tabs-mode:nil # End: # vim: set expandtab tabstop=4 shiftwidth=4: