зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1041941 - Add support for templates in moz.build. r=gps
This commit is contained in:
Родитель
d4f000ffea
Коммит
4cfdc807f5
|
@ -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()
|
||||
|
|
Загрузка…
Ссылка в новой задаче