Bug 1132771 - Support and test for reading without a config object; r=glandium

We want the ability to read data from any moz.build file without needing
a full build configuration (running configure). This will enable tools
to consume metadata by merely having a copy of the source code and
nothing more.

This commit creates the EmptyConfig object. It is a config object that -
as its name implies - is empty. It will be used for reading moz.build
files in "no config" mode.

Many moz.build files make assumptions that variables in CONFIG are
defined and that they are strings. We create the EmptyValue type that
behaves like an empty unicode string. Since moz.build files also do some
type checking, we carve an exemption for EmptyValue, just like we do for
None.

We add a test to verify that reading moz.build files in "no config" mode
works. This required some minor changes to existing moz.build files to
make them work in the new execution mode.
This commit is contained in:
Gregory Szorc 2015-02-26 10:21:52 -08:00
parent d6895efa64
commit a0ed178d7a
7 changed files with 91 additions and 10 deletions

View File

@ -95,7 +95,7 @@ flavors = {
'NetBSD': 'netbsd', 'NetBSD': 'netbsd',
'OpenBSD': 'openbsd', 'OpenBSD': 'openbsd',
} }
gyp_vars['OS'] = flavors[os] gyp_vars['OS'] = flavors.get(os)
arches = { arches = {
'x86_64': 'x64', 'x86_64': 'x64',

View File

@ -10,7 +10,10 @@ import unittest
from mozunit import main from mozunit import main
from mozbuild.base import MozbuildObject from mozbuild.base import MozbuildObject
from mozbuild.frontend.reader import BuildReader from mozbuild.frontend.reader import (
BuildReader,
EmptyConfig,
)
class TestMozbuildReading(unittest.TestCase): class TestMozbuildReading(unittest.TestCase):
@ -24,6 +27,12 @@ class TestMozbuildReading(unittest.TestCase):
os.environ.clear() os.environ.clear()
os.environ.update(self._old_env) os.environ.update(self._old_env)
def _mozbuilds(self, reader):
if not hasattr(self, '_mozbuild_paths'):
self._mozbuild_paths = set(reader.all_mozbuild_paths())
return self._mozbuild_paths
def test_filesystem_traversal_reading(self): def test_filesystem_traversal_reading(self):
"""Reading moz.build according to filesystem traversal works. """Reading moz.build according to filesystem traversal works.
@ -34,11 +43,27 @@ class TestMozbuildReading(unittest.TestCase):
mb = MozbuildObject.from_environment(detect_virtualenv_mozinfo=False) mb = MozbuildObject.from_environment(detect_virtualenv_mozinfo=False)
config = mb.config_environment config = mb.config_environment
reader = BuildReader(config) reader = BuildReader(config)
all_paths = set(reader.all_mozbuild_paths()) all_paths = self._mozbuilds(reader)
paths, contexts = reader.read_relevant_mozbuilds(all_paths) paths, contexts = reader.read_relevant_mozbuilds(all_paths)
self.assertEqual(set(paths), all_paths) self.assertEqual(set(paths), all_paths)
self.assertGreaterEqual(len(contexts), len(paths)) self.assertGreaterEqual(len(contexts), len(paths))
def test_filesystem_traversal_no_config(self):
"""Reading moz.build files via filesystem traversal mode with no build config.
This is similar to the above test except no build config is applied.
This will likely fail in more scenarios than the above test because a
lot of moz.build files assumes certain variables are present.
"""
here = os.path.abspath(os.path.dirname(__file__))
root = os.path.normpath(os.path.join(here, '..', '..'))
config = EmptyConfig(root)
reader = BuildReader(config)
all_paths = self._mozbuilds(reader)
paths, contexts = reader.read_relevant_mozbuilds(all_paths)
self.assertEqual(set(paths.keys()), all_paths)
self.assertGreaterEqual(len(contexts), len(paths))
if __name__ == '__main__': if __name__ == '__main__':
main() main()

View File

@ -87,7 +87,7 @@ elif CONFIG['OS_ARCH'] in ('DragonFly', 'FreeBSD', 'NetBSD', 'OpenBSD'):
UNIFIED_SOURCES += [ UNIFIED_SOURCES += [
'ProcessUtils_bsd.cpp' 'ProcessUtils_bsd.cpp'
] ]
elif CONFIG['OS_ARCH'] in ('Darwin'): elif CONFIG['OS_ARCH'] == 'Darwin':
UNIFIED_SOURCES += [ UNIFIED_SOURCES += [
'ProcessUtils_mac.mm' 'ProcessUtils_mac.mm'
] ]

View File

@ -66,7 +66,7 @@ if CONFIG['COMPILE_ENVIRONMENT'] and not CONFIG['LIBXUL_SDK']:
DIRS += ['config/external/icu'] DIRS += ['config/external/icu']
DIRS += ['js/src'] DIRS += ['js/src']
if not CONFIG['JS_STANDALONE']: if not CONFIG['JS_STANDALONE'] and CONFIG['MOZ_BUILD_APP']:
# Bring in the configuration for the configured application. # Bring in the configuration for the configured application.
include('/' + CONFIG['MOZ_BUILD_APP'] + '/app.mozbuild') include('/' + CONFIG['MOZ_BUILD_APP'] + '/app.mozbuild')

View File

@ -33,7 +33,8 @@ LOCAL_INCLUDES += [
] ]
protocols = CONFIG['NECKO_PROTOCOLS'].copy() protocols = CONFIG['NECKO_PROTOCOLS'].copy()
protocols.remove("about") if 'about' in protocols:
protocols.remove('about')
LOCAL_INCLUDES += sorted([ LOCAL_INCLUDES += sorted([
'/netwerk/protocol/%s' % d for d in protocols '/netwerk/protocol/%s' % d for d in protocols
]) ])

View File

@ -36,6 +36,7 @@ from collections import (
from io import StringIO from io import StringIO
from mozbuild.util import ( from mozbuild.util import (
EmptyValue,
memoize, memoize,
ReadOnlyDefaultDict, ReadOnlyDefaultDict,
ReadOnlyDict, ReadOnlyDict,
@ -82,6 +83,48 @@ def log(logger, level, action, params, formatter):
logger.log(level, formatter, extra={'action': action, 'params': params}) logger.log(level, formatter, extra={'action': action, 'params': params})
class EmptyConfig(object):
"""A config object that is empty.
This config object is suitable for using with a BuildReader on a vanilla
checkout, without any existing configuration. The config is simply
bootstrapped from a top source directory path.
"""
class PopulateOnGetDict(ReadOnlyDefaultDict):
"""A variation on ReadOnlyDefaultDict that populates during .get().
This variation is needed because CONFIG uses .get() to access members.
Without it, None (instead of our EmptyValue types) would be returned.
"""
def get(self, key, default=None):
return self[key]
def __init__(self, topsrcdir):
self.topsrcdir = topsrcdir
self.topobjdir = ''
self.substs = self.PopulateOnGetDict(EmptyValue, {
# These 2 variables are used semi-frequently and it isn't worth
# changing all the instances.
b'MOZ_APP_NAME': b'empty',
b'MOZ_CHILD_PROCESS_NAME': b'empty',
# Set manipulations are performed within the moz.build files. But
# set() is not an exposed symbol, so we can't create an empty set.
b'NECKO_PROTOCOLS': set(),
# Needed to prevent js/src's config.status from loading.
b'JS_STANDALONE': b'1',
})
udict = {}
for k, v in self.substs.items():
if isinstance(v, str):
udict[k.decode('utf-8')] = v.decode('utf-8')
else:
udict[k] = v
self.substs_unicode = self.PopulateOnGetDict(EmptyValue, udict)
self.defines = self.substs
self.external_source_dir = None
def is_read_allowed(path, config): def is_read_allowed(path, config):
"""Whether we are allowed to load a mozbuild file at the specified path. """Whether we are allowed to load a mozbuild file at the specified path.

View File

@ -18,6 +18,7 @@ import os
import stat import stat
import sys import sys
import time import time
import types
from collections import ( from collections import (
defaultdict, defaultdict,
@ -50,6 +51,17 @@ def hash_file(path, hasher=None):
return h.hexdigest() return h.hexdigest()
class EmptyValue(unicode):
"""A dummy type that behaves like an empty string and sequence.
This type exists in order to support
:py:class:`mozbuild.frontend.reader.EmptyConfig`. It should likely not be
used elsewhere.
"""
def __init__(self):
super(EmptyValue, self).__init__()
class ReadOnlyDict(dict): class ReadOnlyDict(dict):
"""A read-only dictionary.""" """A read-only dictionary."""
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -254,9 +266,9 @@ class ListMixin(object):
return super(ListMixin, self).__setslice__(i, j, sequence) return super(ListMixin, self).__setslice__(i, j, sequence)
def __add__(self, other): def __add__(self, other):
# Allow None is a special case because it makes undefined variable # Allow None and EmptyValue is a special case because it makes undefined
# references in moz.build behave better. # variable references in moz.build behave better.
other = [] if other is None else other other = [] if isinstance(other, (types.NoneType, EmptyValue)) else other
if not isinstance(other, list): if not isinstance(other, list):
raise ValueError('Only lists can be appended to lists.') raise ValueError('Only lists can be appended to lists.')
@ -265,7 +277,7 @@ class ListMixin(object):
return new_list return new_list
def __iadd__(self, other): def __iadd__(self, other):
other = [] if other is None else other other = [] if isinstance(other, (types.NoneType, EmptyValue)) else other
if not isinstance(other, list): if not isinstance(other, list):
raise ValueError('Only lists can be appended to lists.') raise ValueError('Only lists can be appended to lists.')