~launchpad-pqm/launchpad/devel

« back to all changes in this revision

Viewing changes to lib/canonical/buildd/pottery/intltool.py

  • Committer: mbp at canonical
  • Date: 2011-11-20 23:37:23 UTC
  • mto: This revision was merged to the branch mainline in revision 14344.
  • Revision ID: mbp@canonical.com-20111120233723-370p96db2crru5tm
Delete canonical.buildd again

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright 2009-2010 Canonical Ltd.  This software is licensed under the
2
 
# GNU Affero General Public License version 3 (see the file LICENSE).
3
 
 
4
 
"""Functions to build PO templates on the build slave."""
5
 
 
6
 
__metaclass__ = type
7
 
__all__ = [
8
 
    'check_potfiles_in',
9
 
    'generate_pot',
10
 
    'generate_pots',
11
 
    'get_translation_domain',
12
 
    'find_intltool_dirs',
13
 
    'find_potfiles_in',
14
 
    ]
15
 
 
16
 
from contextlib import contextmanager
17
 
import errno
18
 
import os.path
19
 
import re
20
 
from subprocess import call
21
 
 
22
 
 
23
 
def find_potfiles_in():
24
 
    """Search the current directory and its subdirectories for POTFILES.in.
25
 
 
26
 
    :returns: A list of names of directories that contain a file POTFILES.in.
27
 
    """
28
 
    result_dirs = []
29
 
    for dirpath, dirnames, dirfiles in os.walk("."):
30
 
        if "POTFILES.in" in dirfiles:
31
 
            result_dirs.append(dirpath)
32
 
    return result_dirs
33
 
 
34
 
 
35
 
def check_potfiles_in(path):
36
 
    """Check if the files listed in the POTFILES.in file exist.
37
 
 
38
 
    Running 'intltool-update -m' will perform this check and also take a
39
 
    possible POTFILES.skip into account. It stores details about 'missing'
40
 
    (files that should be in POTFILES.in) and 'notexist'ing files (files
41
 
    that are listed in POTFILES.in but don't exist) in files which are
42
 
    named accordingly. These files are removed before the run.
43
 
 
44
 
    We don't care about files missing from POTFILES.in but want to know if
45
 
    all listed files exist. The presence of the 'notexist' file tells us
46
 
    that.
47
 
 
48
 
    :param path: The directory where POTFILES.in resides.
49
 
    :returns: False if the directory does not exist, if an error occurred
50
 
        when executing intltool-update or if files are missing from
51
 
        POTFILES.in. True if all went fine and all files in POTFILES.in
52
 
        actually exist.  
53
 
    """
54
 
    current_path = os.getcwd()
55
 
 
56
 
    try:
57
 
        os.chdir(path)
58
 
    except OSError, e:
59
 
        # Abort nicely if the directory does not exist.
60
 
        if e.errno == errno.ENOENT:
61
 
            return False
62
 
        raise
63
 
    try:
64
 
        # Remove stale files from a previous run of intltool-update -m.
65
 
        for unlink_name in ['missing', 'notexist']:
66
 
            try:
67
 
                os.unlink(unlink_name)
68
 
            except OSError, e:
69
 
                # It's ok if the files are missing.
70
 
                if e.errno != errno.ENOENT:
71
 
                    raise
72
 
        devnull = open("/dev/null", "w")
73
 
        returncode = call(
74
 
            ["/usr/bin/intltool-update", "-m"],
75
 
            stdout=devnull, stderr=devnull)
76
 
        devnull.close()
77
 
    finally:
78
 
        os.chdir(current_path)
79
 
 
80
 
    if returncode != 0:
81
 
        # An error occurred when executing intltool-update.
82
 
        return False
83
 
 
84
 
    notexist = os.path.join(path, "notexist")
85
 
    return not os.access(notexist, os.R_OK)
86
 
 
87
 
 
88
 
def find_intltool_dirs():
89
 
    """Search for directories with intltool structure.
90
 
 
91
 
    The current directory and its subdiretories are searched. An 'intltool
92
 
    structure' is a directory that contains a POFILES.in file and where all
93
 
    files listed in that POTFILES.in do actually exist. The latter
94
 
    condition makes sure that the file is not stale.
95
 
 
96
 
    :returns: A list of directory names.
97
 
    """
98
 
    return sorted(filter(check_potfiles_in, find_potfiles_in()))
99
 
 
100
 
 
101
 
def _get_AC_PACKAGE_NAME(config_file):
102
 
    """Get the value of AC_PACKAGE_NAME from function parameters.
103
 
 
104
 
    The value of AC_PACKAGE_NAME is either the first or the fourth
105
 
    parameter of the AC_INIT call if it is called with at least two
106
 
    parameters.
107
 
    """
108
 
    params = config_file.getFunctionParams("AC_INIT")
109
 
    if params is None or len(params) < 2:
110
 
        return None
111
 
    if len(params) < 4:
112
 
        return params[0]
113
 
    else:
114
 
        return params[3]
115
 
 
116
 
 
117
 
def _try_substitution(config_files, varname, substitution):
118
 
    """Try to find a substitution in the config files.
119
 
 
120
 
    :returns: The completed substitution or None if none was found.
121
 
    """
122
 
    subst_value = None
123
 
    if varname == substitution.name:
124
 
        # Do not look for the same name in the current file.
125
 
        config_files = config_files[:-1]
126
 
    for config_file in reversed(config_files):
127
 
        subst_value = config_file.getVariable(substitution.name)
128
 
        if subst_value is not None:
129
 
            # Substitution found.
130
 
            break
131
 
    else:
132
 
        # No substitution found.
133
 
        return None
134
 
    return substitution.replace(subst_value)
135
 
 
136
 
 
137
 
def get_translation_domain(dirname):
138
 
    """Get the translation domain for this PO directory.
139
 
 
140
 
    Imitates some of the behavior of intltool-update to find out which
141
 
    translation domain the build environment provides. The domain is usually
142
 
    defined in the GETTEXT_PACKAGE variable in one of the build files. Another
143
 
    variant is DOMAIN in the Makevars file. This function goes through the
144
 
    ordered list of these possible locations, top to bottom, and tries to
145
 
    find a valid value. Since the same variable name may be defined in
146
 
    multiple files (usually configure.ac and Makefile.in.in), it needs to
147
 
    keep trying with the next file, until it finds the most specific
148
 
    definition.
149
 
 
150
 
    If the found value contains a substitution, either autoconf style (@...@)
151
 
    or make style ($(...)), the search is continued in the same file and back
152
 
    up the list of files, now searching for the substitution. Multiple
153
 
    substitutions or multi-level substitutions are not supported.
154
 
    """
155
 
    locations = [
156
 
        ('../configure.ac', 'GETTEXT_PACKAGE', True),
157
 
        ('../configure.in', 'GETTEXT_PACKAGE', True),
158
 
        ('Makefile.in.in', 'GETTEXT_PACKAGE', False),
159
 
        ('Makevars', 'DOMAIN', False),
160
 
    ]
161
 
    value = None
162
 
    substitution = None
163
 
    config_files = []
164
 
    for filename, varname, keep_trying in locations:
165
 
        path = os.path.join(dirname, filename)
166
 
        if not os.access(path, os.R_OK):
167
 
            # Skip non-existent files.
168
 
            continue
169
 
        config_files.append(ConfigFile(path))
170
 
        new_value = config_files[-1].getVariable(varname)
171
 
        if new_value is not None:
172
 
            value = new_value
173
 
            if value == "AC_PACKAGE_NAME":
174
 
                value = _get_AC_PACKAGE_NAME(config_files[-1])
175
 
            else:
176
 
                # Check if the value needs a substitution.
177
 
                substitution = Substitution.get(value)
178
 
                if substitution is not None:
179
 
                    # Try to substitute with value.
180
 
                    value = _try_substitution(
181
 
                        config_files, varname, substitution)
182
 
                    if value is None:
183
 
                        # No substitution found; the setup is broken.
184
 
                        break
185
 
        if value is not None and not keep_trying:
186
 
            # A value has been found.
187
 
            break
188
 
    return value
189
 
 
190
 
 
191
 
@contextmanager
192
 
def chdir(directory):
193
 
    cwd = os.getcwd()
194
 
    os.chdir(directory)
195
 
    yield
196
 
    os.chdir(cwd)
197
 
 
198
 
 
199
 
def generate_pot(podir, domain):
200
 
    """Generate one PO template using intltool.
201
 
 
202
 
    Although 'intltool-update -p' can try to find out the translation domain
203
 
    we trust our own code more on this one and simply specify the domain.
204
 
    Also, the man page for 'intltool-update' states that the '-g' option
205
 
    "has an additional effect: the name of current working directory is no
206
 
    more  limited  to 'po' or 'po-*'." We don't want that limit either.
207
 
 
208
 
    :param podir: The PO directory in which to build template.
209
 
    :param domain: The translation domain to use as the name of the template.
210
 
      If it is None or empty, 'messages.pot' will be used.
211
 
    :return: True if generation succeeded.
212
 
    """
213
 
    if domain is None or domain.strip() == "":
214
 
        domain = "messages"
215
 
    with chdir(podir):
216
 
        with open("/dev/null", "w") as devnull:
217
 
            returncode = call(
218
 
                ["/usr/bin/intltool-update", "-p", "-g", domain],
219
 
                stdout=devnull, stderr=devnull)
220
 
    return returncode == 0
221
 
 
222
 
 
223
 
def generate_pots(package_dir='.'):
224
 
    """Top-level function to generate all PO templates in a package."""
225
 
    potpaths = []
226
 
    with chdir(package_dir):
227
 
        for podir in find_intltool_dirs():
228
 
            domain = get_translation_domain(podir)
229
 
            if generate_pot(podir, domain):
230
 
                potpaths.append(os.path.join(podir, domain + ".pot"))
231
 
    return potpaths
232
 
 
233
 
 
234
 
class ConfigFile(object):
235
 
    """Represent a config file and return variables defined in it."""
236
 
 
237
 
    def __init__(self, file_or_name):
238
 
        if isinstance(file_or_name, basestring):
239
 
            conf_file = file(file_or_name)
240
 
        else:
241
 
            conf_file = file_or_name
242
 
        self.content = conf_file.read()
243
 
 
244
 
    def _stripQuotes(self, identifier):
245
 
        """Strip surrounding quotes from `identifier`, if present.
246
 
 
247
 
        :param identifier: a string, possibly surrounded by matching
248
 
            'single,' "double," or [bracket] quotes.
249
 
        :return: `identifier` but with the outer pair of matching quotes
250
 
            removed, if they were there.
251
 
        """
252
 
        if len(identifier) < 2:
253
 
            return identifier
254
 
 
255
 
        quote_pairs = [
256
 
            ('"', '"'),
257
 
            ("'", "'"),
258
 
            ("[", "]"),
259
 
            ]
260
 
        for (left, right) in quote_pairs:
261
 
            if identifier.startswith(left) and identifier.endswith(right):
262
 
                return identifier[1:-1]
263
 
 
264
 
        return identifier
265
 
 
266
 
    def getVariable(self, name):
267
 
        """Search the file for a variable definition with this name."""
268
 
        pattern = re.compile(
269
 
            "^%s[ \t]*=[ \t]*([^\s]*)" % re.escape(name), re.M)
270
 
        result = pattern.search(self.content)
271
 
        if result is None:
272
 
            return None
273
 
        return self._stripQuotes(result.group(1))
274
 
 
275
 
    def getFunctionParams(self, name):
276
 
        """Search file for a function call with this name, return parameters.
277
 
        """
278
 
        pattern = re.compile("^%s\(([^)]*)\)" % re.escape(name), re.M)
279
 
        result = pattern.search(self.content)
280
 
        if result is None:
281
 
            return None
282
 
        else:
283
 
            return [
284
 
                self._stripQuotes(param.strip())
285
 
                for param in result.group(1).split(',')
286
 
                ]
287
 
 
288
 
 
289
 
class Substitution(object):
290
 
    """Find and replace substitutions.
291
 
 
292
 
    Variable texts may contain other variables which should be substituted
293
 
    for their value. These are either marked by surrounding @ signs (autoconf
294
 
    style) or preceded by a $ sign with optional () (make style).
295
 
 
296
 
    This class identifies a single such substitution in a variable text and
297
 
    extract the name of the variable who's value is to be inserted. It also
298
 
    facilitates the actual replacement so that caller does not have to worry
299
 
    about the substitution style that is being used.
300
 
    """
301
 
 
302
 
    autoconf_pattern = re.compile("@([^@]+)@")
303
 
    makefile_pattern = re.compile("\$\(?([^\s\)]+)\)?")
304
 
 
305
 
    @staticmethod
306
 
    def get(variabletext):
307
 
        """Factory method.
308
 
 
309
 
        Creates a Substitution instance and checks if it found a substitution.
310
 
 
311
 
        :param variabletext: A variable value with possible substitution.
312
 
        :returns: A Substitution object or None if no substitution was found.
313
 
        """
314
 
        subst = Substitution(variabletext)
315
 
        if subst.name is not None:
316
 
            return subst
317
 
        return None
318
 
 
319
 
    def _searchForPatterns(self):
320
 
        """Search for all the available patterns in variable text."""
321
 
        result = self.autoconf_pattern.search(self.text)
322
 
        if result is None:
323
 
            result = self.makefile_pattern.search(self.text)
324
 
        return result
325
 
 
326
 
    def __init__(self, variabletext):
327
 
        """Extract substitution name from variable text."""
328
 
        self.text = variabletext
329
 
        self.replaced = False
330
 
        result = self._searchForPatterns()
331
 
        if result is None:
332
 
            self._replacement = None
333
 
            self.name = None
334
 
        else:
335
 
            self._replacement = result.group(0)
336
 
            self.name = result.group(1)
337
 
 
338
 
    def replace(self, value):
339
 
        """Return a copy of the variable text with the substitution resolved.
340
 
        """
341
 
        self.replaced = True
342
 
        return self.text.replace(self._replacement, value)