Bug 1041941 - Add support for templates in moz.build. r=gps

This commit is contained in:
Mike Hommey 2014-08-24 09:11:05 +09:00
Родитель d4f000ffea
Коммит 4cfdc807f5
4 изменённых файлов: 331 добавлений и 12 удалений

Просмотреть файл

@ -950,12 +950,13 @@ for name, (storage_type, input_types, docs, tier) in VARIABLES.items():
#
# Each entry is a tuple of:
#
# (method attribute, (argument types), docs)
# (function returning the corresponding function from a given sandbox,
# (argument types), docs)
#
# The first element is an attribute on Sandbox that should be a function type.
#
FUNCTIONS = {
'include': ('_include', (str,),
'include': (lambda self: self._include, (str,),
"""Include another mozbuild file in the context of this one.
This is similar to a ``#include`` in C languages. The filename passed to
@ -982,7 +983,7 @@ FUNCTIONS = {
include('/elsewhere/foo.build')
"""),
'add_java_jar': ('_add_java_jar', (str,),
'add_java_jar': (lambda self: self._add_java_jar, (str,),
"""Declare a Java JAR target to be built.
This is the supported way to populate the JAVA_JAR_TARGETS
@ -995,7 +996,8 @@ FUNCTIONS = {
:py:class:`mozbuild.frontend.data.JavaJarData`.
"""),
'add_android_eclipse_project': ('_add_android_eclipse_project', (str, str),
'add_android_eclipse_project': (
lambda self: self._add_android_eclipse_project, (str, str),
"""Declare an Android Eclipse project.
This is one of the supported ways to populate the
@ -1009,7 +1011,8 @@ FUNCTIONS = {
:py:class:`mozbuild.frontend.data.AndroidEclipseProjectData`.
"""),
'add_android_eclipse_library_project': ('_add_android_eclipse_library_project', (str,),
'add_android_eclipse_library_project': (
lambda self: self._add_android_eclipse_library_project, (str,),
"""Declare an Android Eclipse library project.
This is one of the supported ways to populate the
@ -1022,7 +1025,9 @@ FUNCTIONS = {
:py:class:`mozbuild.frontend.data.AndroidEclipseProjectData`.
"""),
'add_tier_dir': ('_add_tier_directory', (str, [str, list], bool, bool, str),
'add_tier_dir': (
lambda self: self._add_tier_directory,
(str, [str, list], bool, bool, str),
"""Register a directory for tier traversal.
This is the preferred way to populate the TIERS variable.
@ -1057,7 +1062,7 @@ FUNCTIONS = {
add_tier_dir('base', 'bar', external=True)
"""),
'export': ('_export', (str,),
'export': (lambda self: self._export, (str,),
"""Make the specified variable available to all child directories.
The variable specified by the argument string is added to the
@ -1084,7 +1089,7 @@ FUNCTIONS = {
export('XPI_NAME')
"""),
'warning': ('_warning', (str,),
'warning': (lambda self: self._warning, (str,),
"""Issue a warning.
Warnings are string messages that are printed during execution.
@ -1092,11 +1097,58 @@ FUNCTIONS = {
Warnings are ignored during execution.
"""),
'error': ('_error', (str,),
'error': (lambda self: self._error, (str,),
"""Issue a fatal error.
If this function is called, processing is aborted immediately.
"""),
'template': (lambda self: self._template_decorator, (),
"""Decorator for template declarations.
Templates are a special kind of functions that can be declared in
mozbuild files. Uppercase variables assigned in the function scope
are considered to be the result of the template.
Contrary to traditional python functions:
- return values from template functions are ignored,
- template functions don't have access to the global scope.
Example template
^^^^^^^^^^^^^^^^
The following ``Program`` template sets two variables ``PROGRAM`` and
``USE_LIBS``. ``PROGRAM`` is set to the argument given on the template
invocation, and ``USE_LIBS`` to contain "mozglue"::
@template
def Program(name):
PROGRAM = name
USE_LIBS += ['mozglue']
Template invocation
^^^^^^^^^^^^^^^^^^^
A template is invoked in the form of a function call::
Program('myprog')
The result of the template, being all the uppercase variable it sets
is mixed to the existing set of variables defined in the mozbuild file
invoking the template::
FINAL_TARGET = 'dist/other'
USE_LIBS += ['mylib']
Program('myprog')
USE_LIBS += ['otherlib']
The above mozbuild results in the following variables set:
- ``FINAL_TARGET`` is 'dist/other'
- ``USE_LIBS`` is ['mylib', 'mozglue', 'otherlib']
- ``PROGRAM`` is 'myprog'
"""),
}
# Special variables. These complement VARIABLES.

Просмотреть файл

@ -18,10 +18,12 @@ It does this by examining specific variables populated during execution.
from __future__ import print_function, unicode_literals
import inspect
import logging
import os
import sys
import time
import tokenize
import traceback
import types
@ -29,6 +31,7 @@ from collections import OrderedDict
from io import StringIO
from mozbuild.util import (
memoize,
ReadOnlyDefaultDict,
ReadOnlyDict,
)
@ -126,12 +129,15 @@ class MozbuildSandbox(Sandbox):
exports = self.metadata.get('exports', {})
self.exports = set(exports.keys())
context.update(exports)
self.templates = self.metadata.setdefault('templates', {})
def __getitem__(self, key):
if key in SPECIAL_VARIABLES:
return SPECIAL_VARIABLES[key][0](self._context)
if key in FUNCTIONS:
return getattr(self, FUNCTIONS[key][0])
return FUNCTIONS[key][0](self)
if key in self.templates:
return self._create_template_function(self.templates[key])
return Sandbox.__getitem__(self, key)
def __setitem__(self, key, value):
@ -298,6 +304,106 @@ class MozbuildSandbox(Sandbox):
def _error(self, message):
raise SandboxCalledError(self._execution_stack, message)
def _template_decorator(self, func):
"""Registers template as expected by _create_template_function.
The template data consists of:
- the function object as it comes from the sandbox evaluation of the
template declaration.
- its code, modified as described in the comments of this method.
- the path of the file containing the template definition.
"""
if not inspect.isfunction(func):
raise Exception('`template` is a function decorator. You must '
'use it as `@template` preceding a function declaration.')
name = func.func_name
if name in self.templates:
raise KeyError(
'A template named "%s" was already declared in %s.' % (name,
self.templates[name][2]))
if name.islower() or name.isupper() or name[0].islower():
raise NameError('Template function names must be CamelCase.')
lines, firstlineno = inspect.getsourcelines(func)
first_op = None
generator = tokenize.generate_tokens(iter(lines).next)
# Find the first indent token in the source of this template function,
# which corresponds to the beginning of the function body.
for typ, s, begin, end, line in generator:
if typ == tokenize.OP:
first_op = True
if first_op and typ == tokenize.INDENT:
break
if typ != tokenize.INDENT:
# This should never happen.
raise Exception('Could not find the first line of the template %s' %
func.func_name)
# The code of the template in moz.build looks like this:
# m def Foo(args):
# n FOO = 'bar'
# n+1 (...)
#
# where,
# - m is firstlineno - 1,
# - n is usually m + 1, but in case the function signature takes more
# lines, is really m + begin[0] - 1
#
# We want that to be replaced with:
# m if True:
# n FOO = 'bar'
# n+1 (...)
#
# (this is simpler than trying to deindent the function body)
# So we need to prepend with n - 1 newlines so that line numbers
# are unchanged.
code = '\n' * (firstlineno + begin[0] - 3) + 'if True:\n'
code += ''.join(lines[begin[0] - 1:])
self.templates[name] = func, code, self._execution_stack[-1]
@memoize
def _create_template_function(self, template):
"""Returns a function object for use within the sandbox for the given
template.
When a moz.build file contains a reference to a template call, the
sandbox needs a function to execute. This is what this method returns.
That function creates a new sandbox for execution of the template.
After the template is executed, the data from its execution is merged
with the context of the calling sandbox.
"""
func, code, path = template
def template_function(*args, **kwargs):
context = Context(VARIABLES, self._context.config)
context.add_source(self._execution_stack[-1])
for p in self._context.all_paths:
context.add_source(p)
sandbox = MozbuildSandbox(context, self.metadata)
for k, v in inspect.getcallargs(func, *args, **kwargs).items():
sandbox[k] = v
sandbox.exec_source(code, path)
# The sandbox will do all the necessary checks for these merges.
for key, value in context.items():
if isinstance(value, dict):
self[key].update(value)
elif isinstance(value, list):
self[key] += value
else:
self[key] = value
for p in context.all_paths:
self._context.add_source(p)
return template_function
class SandboxValidationError(Exception):
"""Represents an error encountered when validating sandbox results."""
@ -846,6 +952,9 @@ class BuildReader(object):
d, var), context)
recurse_info[d] = {}
if 'templates' in sandbox.metadata:
recurse_info[d]['templates'] = dict(
sandbox.metadata['templates'])
if 'exports' in sandbox.metadata:
sandbox.recompute_exports()
recurse_info[d]['exports'] = dict(sandbox.metadata['exports'])
@ -865,6 +974,9 @@ class BuildReader(object):
'Tier directory (%s) registered multiple '
'times in %s' % (d, tier), context)
recurse_info[d] = {'check_external': True}
if 'templates' in sandbox.metadata:
recurse_info[d]['templates'] = dict(
sandbox.metadata['templates'])
for relpath, child_metadata in recurse_info.items():
if 'check_external' in child_metadata:

Просмотреть файл

@ -0,0 +1,21 @@
@template
def Template(foo, bar=[]):
SOURCES += foo
DIRS += bar
@template
def TemplateError(foo):
ILLEGAL = foo
@template
def TemplateGlobalVariable():
SOURCES += illegal
@template
def TemplateGlobalUPPERVariable():
SOURCES += DIRS
@template
def TemplateInherit(foo):
USE_LIBS += ['foo']
Template(foo)

Просмотреть файл

@ -120,7 +120,7 @@ class TestSandbox(unittest.TestCase):
class TestMozbuildSandbox(unittest.TestCase):
def sandbox(self, data_path=None):
def sandbox(self, data_path=None, metadata={}):
config = None
if data_path is not None:
@ -128,7 +128,7 @@ class TestMozbuildSandbox(unittest.TestCase):
else:
config = MockConfig()
return MozbuildSandbox(Context(VARIABLES, config))
return MozbuildSandbox(Context(VARIABLES, config), metadata)
def test_default_state(self):
sandbox = self.sandbox()
@ -358,5 +358,139 @@ add_tier_dir('t1', 'bat')
self.assertEqual(se.exception.exc_type, ValueError)
def test_templates(self):
sandbox = self.sandbox(data_path='templates')
# Templates need to be defined in actual files because of
# inspect.getsourcelines.
sandbox.exec_file('templates.mozbuild')
sandbox2 = self.sandbox(metadata={'templates': sandbox.templates})
source = '''
Template([
'foo.cpp',
])
'''
sandbox2.exec_source(source, sandbox.normalize_path('foo.mozbuild'))
self.assertEqual(sandbox2._context, {
'SOURCES': ['foo.cpp'],
'DIRS': [],
})
sandbox2 = self.sandbox(metadata={'templates': sandbox.templates})
source = '''
SOURCES += ['qux.cpp']
Template([
'bar.cpp',
'foo.cpp',
],[
'foo',
])
SOURCES += ['hoge.cpp']
'''
sandbox2.exec_source(source, sandbox.normalize_path('foo.mozbuild'))
self.assertEqual(sandbox2._context, {
'SOURCES': ['qux.cpp', 'bar.cpp', 'foo.cpp', 'hoge.cpp'],
'DIRS': ['foo'],
})
source = '''
TemplateError([
'foo.cpp',
])
'''
with self.assertRaises(SandboxExecutionError) as se:
sandbox2.exec_source(source, sandbox.normalize_path('foo.mozbuild'))
e = se.exception
self.assertIsInstance(e.exc_value, KeyError)
e = se.exception.exc_value
self.assertEqual(e.args[0], 'global_ns')
self.assertEqual(e.args[1], 'set_unknown')
# TemplateGlobalVariable tries to access 'illegal' but that is expected
# to throw.
source = '''
illegal = True
TemplateGlobalVariable()
'''
with self.assertRaises(SandboxExecutionError) as se:
sandbox2.exec_source(source, sandbox.normalize_path('foo.mozbuild'))
e = se.exception
self.assertIsInstance(e.exc_value, NameError)
# TemplateGlobalUPPERVariable sets SOURCES with DIRS, but the context
# used when running the template is not expected to access variables
# from the global context.
sandbox2 = self.sandbox(metadata={'templates': sandbox.templates})
source = '''
DIRS += ['foo']
TemplateGlobalUPPERVariable()
'''
sandbox2.exec_source(source, sandbox.normalize_path('foo.mozbuild'))
self.assertEqual(sandbox2._context, {
'SOURCES': [],
'DIRS': ['foo'],
})
# However, the result of the template is mixed with the global
# context.
sandbox2 = self.sandbox(metadata={'templates': sandbox.templates})
source = '''
SOURCES += ['qux.cpp']
TemplateInherit([
'bar.cpp',
'foo.cpp',
])
SOURCES += ['hoge.cpp']
'''
sandbox2.exec_source(source, sandbox.normalize_path('foo.mozbuild'))
self.assertEqual(sandbox2._context, {
'SOURCES': ['qux.cpp', 'bar.cpp', 'foo.cpp', 'hoge.cpp'],
'USE_LIBS': ['foo'],
'DIRS': [],
})
# Template names must be CamelCase. Here, we can define the template
# inline because the error happens before inspect.getsourcelines.
source = '''
@template
def foo():
pass
'''
with self.assertRaises(SandboxExecutionError) as se:
sandbox2.exec_source(source, sandbox.normalize_path('foo.mozbuild'))
e = se.exception
self.assertIsInstance(e.exc_value, NameError)
e = se.exception.exc_value
self.assertEqual(e.message,
'Template function names must be CamelCase.')
# Template names must not already be registered.
source = '''
@template
def Template():
pass
'''
with self.assertRaises(SandboxExecutionError) as se:
sandbox2.exec_source(source, sandbox.normalize_path('foo.mozbuild'))
e = se.exception
self.assertIsInstance(e.exc_value, KeyError)
e = se.exception.exc_value
self.assertEqual(e.message,
'A template named "Template" was already declared in %s.' %
sandbox.normalize_path('templates.mozbuild'))
if __name__ == '__main__':
main()