1
# Copyright 2009-2010 Canonical Ltd. This software is licensed under the
2
# GNU Affero General Public License version 3 (see the file LICENSE).
4
"""Functions to build PO templates on the build slave."""
11
'get_translation_domain',
16
from contextlib import contextmanager
20
from subprocess import call
23
def find_potfiles_in():
24
"""Search the current directory and its subdirectories for POTFILES.in.
26
:returns: A list of names of directories that contain a file POTFILES.in.
29
for dirpath, dirnames, dirfiles in os.walk("."):
30
if "POTFILES.in" in dirfiles:
31
result_dirs.append(dirpath)
35
def check_potfiles_in(path):
36
"""Check if the files listed in the POTFILES.in file exist.
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.
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
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
54
current_path = os.getcwd()
59
# Abort nicely if the directory does not exist.
60
if e.errno == errno.ENOENT:
64
# Remove stale files from a previous run of intltool-update -m.
65
for unlink_name in ['missing', 'notexist']:
67
os.unlink(unlink_name)
69
# It's ok if the files are missing.
70
if e.errno != errno.ENOENT:
72
devnull = open("/dev/null", "w")
74
["/usr/bin/intltool-update", "-m"],
75
stdout=devnull, stderr=devnull)
78
os.chdir(current_path)
81
# An error occurred when executing intltool-update.
84
notexist = os.path.join(path, "notexist")
85
return not os.access(notexist, os.R_OK)
88
def find_intltool_dirs():
89
"""Search for directories with intltool structure.
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.
96
:returns: A list of directory names.
98
return sorted(filter(check_potfiles_in, find_potfiles_in()))
101
def _get_AC_PACKAGE_NAME(config_file):
102
"""Get the value of AC_PACKAGE_NAME from function parameters.
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
108
params = config_file.getFunctionParams("AC_INIT")
109
if params is None or len(params) < 2:
117
def _try_substitution(config_files, varname, substitution):
118
"""Try to find a substitution in the config files.
120
:returns: The completed substitution or None if none was found.
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.
132
# No substitution found.
134
return substitution.replace(subst_value)
137
def get_translation_domain(dirname):
138
"""Get the translation domain for this PO directory.
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
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.
156
('../configure.ac', 'GETTEXT_PACKAGE', True),
157
('../configure.in', 'GETTEXT_PACKAGE', True),
158
('Makefile.in.in', 'GETTEXT_PACKAGE', False),
159
('Makevars', 'DOMAIN', False),
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.
169
config_files.append(ConfigFile(path))
170
new_value = config_files[-1].getVariable(varname)
171
if new_value is not None:
173
if value == "AC_PACKAGE_NAME":
174
value = _get_AC_PACKAGE_NAME(config_files[-1])
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)
183
# No substitution found; the setup is broken.
185
if value is not None and not keep_trying:
186
# A value has been found.
192
def chdir(directory):
199
def generate_pot(podir, domain):
200
"""Generate one PO template using intltool.
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.
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.
213
if domain is None or domain.strip() == "":
216
with open("/dev/null", "w") as devnull:
218
["/usr/bin/intltool-update", "-p", "-g", domain],
219
stdout=devnull, stderr=devnull)
220
return returncode == 0
223
def generate_pots(package_dir='.'):
224
"""Top-level function to generate all PO templates in a package."""
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"))
234
class ConfigFile(object):
235
"""Represent a config file and return variables defined in it."""
237
def __init__(self, file_or_name):
238
if isinstance(file_or_name, basestring):
239
conf_file = file(file_or_name)
241
conf_file = file_or_name
242
self.content = conf_file.read()
244
def _stripQuotes(self, identifier):
245
"""Strip surrounding quotes from `identifier`, if present.
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.
252
if len(identifier) < 2:
260
for (left, right) in quote_pairs:
261
if identifier.startswith(left) and identifier.endswith(right):
262
return identifier[1:-1]
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)
273
return self._stripQuotes(result.group(1))
275
def getFunctionParams(self, name):
276
"""Search file for a function call with this name, return parameters.
278
pattern = re.compile("^%s\(([^)]*)\)" % re.escape(name), re.M)
279
result = pattern.search(self.content)
284
self._stripQuotes(param.strip())
285
for param in result.group(1).split(',')
289
class Substitution(object):
290
"""Find and replace substitutions.
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).
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.
302
autoconf_pattern = re.compile("@([^@]+)@")
303
makefile_pattern = re.compile("\$\(?([^\s\)]+)\)?")
306
def get(variabletext):
309
Creates a Substitution instance and checks if it found a substitution.
311
:param variabletext: A variable value with possible substitution.
312
:returns: A Substitution object or None if no substitution was found.
314
subst = Substitution(variabletext)
315
if subst.name is not None:
319
def _searchForPatterns(self):
320
"""Search for all the available patterns in variable text."""
321
result = self.autoconf_pattern.search(self.text)
323
result = self.makefile_pattern.search(self.text)
326
def __init__(self, variabletext):
327
"""Extract substitution name from variable text."""
328
self.text = variabletext
329
self.replaced = False
330
result = self._searchForPatterns()
332
self._replacement = None
335
self._replacement = result.group(0)
336
self.name = result.group(1)
338
def replace(self, value):
339
"""Return a copy of the variable text with the substitution resolved.
342
return self.text.replace(self._replacement, value)