Bug 1160563 - Part 1: Make ANDROID_RES_DIRS a moz.build variable. r=gps, a=sledru
authorNick Alexander <nalexander@mozilla.com>
Wed, 12 Aug 2015 11:03:44 -0700
changeset 289312 84e994df984952273c452887f1effd7756023707
parent 289311 fbb02d5e678489003d9587410d807bfe3e1158fa
child 289313 39fcc0e11d27adc301394a094a5db2f3c9fd41b8
push id5067
push userraliiev@mozilla.com
push dateMon, 21 Sep 2015 14:04:52 +0000
treeherdermozilla-beta@14221ffe5b2f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersgps, sledru
bugs1160563
milestone42.0a2
Bug 1160563 - Part 1: Make ANDROID_RES_DIRS a moz.build variable. r=gps, a=sledru This patch does a few things. First, it adds an AbsolutePath data type, sibling to SourcePath and ObjDirPath. (Existing Path consumers that accept an open set of Path subtypes, and that only use full_path, should function fine with the new AbsolutePath subtype.) Second, it moves ANDROID_RES_DIRS to a moz.build list of Paths (ordered). We test, but don't use in tree, the new AbsolutePath.
mobile/android/base/moz.build
python/mozbuild/mozbuild/backend/recursivemake.py
python/mozbuild/mozbuild/frontend/context.py
python/mozbuild/mozbuild/frontend/data.py
python/mozbuild/mozbuild/frontend/emitter.py
python/mozbuild/mozbuild/test/frontend/data/android-res-dirs/dir1/foo
python/mozbuild/mozbuild/test/frontend/data/android-res-dirs/moz.build
python/mozbuild/mozbuild/test/frontend/test_context.py
python/mozbuild/mozbuild/test/frontend/test_emitter.py
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -573,17 +573,17 @@ gbjar.sources += [ thirdparty_source_dir
 ] ]
 android_package_dir = CONFIG['ANDROID_PACKAGE_NAME'].replace('.', '/')
 gbjar.generated_sources = [] # Keep it this way.
 gbjar.extra_jars += [
     'constants.jar'
 ]
 if CONFIG['MOZ_CRASHREPORTER']:
     gbjar.sources += [ 'CrashReporter.java' ]
-    ANDROID_RES_DIRS += [ SRCDIR + '/crashreporter/res' ]
+    ANDROID_RES_DIRS += [ 'crashreporter/res' ]
 
 if CONFIG['MOZ_ANDROID_SHARE_OVERLAY']:
     gbjar.sources += [
         'overlays/OverlayConstants.java',
         'overlays/service/OverlayActionService.java',
         'overlays/service/ShareData.java',
         'overlays/service/sharemethods/AddBookmark.java',
         'overlays/service/sharemethods/AddToReadingList.java',
@@ -748,19 +748,19 @@ if CONFIG['MOZ_INSTALL_TRACKING']:
         'com/adjust/sdk/Reflection.java',
         'com/adjust/sdk/RequestHandler.java',
         'com/adjust/sdk/UnitTestActivity.java',
         'com/adjust/sdk/Util.java'
     ] ]
 
 # Putting branding earlier allows branders to override default resources.
 ANDROID_RES_DIRS += [
-    TOPSRCDIR + '/' + CONFIG['MOZ_BRANDING_DIRECTORY'] + '/res',
-    SRCDIR + '/resources',
-    OBJDIR + '/res',
+    '/' + CONFIG['MOZ_BRANDING_DIRECTORY'] + '/res',
+    'resources',
+    '!res',
 ]
 
 ANDROID_GENERATED_RESFILES += [
     'res/raw/suggestedsites.json',
     'res/values/strings.xml',
 ]
 
 # We do not expose MOZ_INSTALL_TRACKING_ADJUST_SDK_APP_TOKEN here because that
--- a/python/mozbuild/mozbuild/backend/recursivemake.py
+++ b/python/mozbuild/mozbuild/backend/recursivemake.py
@@ -21,16 +21,17 @@ from reftest import ReftestManifest
 from mozpack.copier import FilePurger
 from mozpack.manifests import (
     InstallManifest,
 )
 import mozpack.path as mozpath
 
 from .common import CommonBackend
 from ..frontend.data import (
+    AndroidResDirs,
     AndroidEclipseProjectData,
     BrandingFiles,
     ConfigFileSubstitution,
     ContextDerived,
     ContextWrapped,
     Defines,
     DistFiles,
     DirectoryTraversal,
@@ -593,16 +594,20 @@ class RecursiveMakeBackend(CommonBackend
         elif isinstance(obj, DistFiles):
             # We'd like to install these via manifests as preprocessed files.
             # But they currently depend on non-standard flags being added via
             # some Makefiles, so for now we just pass them through to the
             # underlying Makefile.in.
             for f in obj.files:
                 backend_file.write('DIST_FILES += %s\n' % f)
 
+        elif isinstance(obj, AndroidResDirs):
+            for p in obj.paths:
+                backend_file.write('ANDROID_RES_DIRS += %s\n' % p.full_path)
+
         else:
             return
         obj.ack()
 
     def _fill_root_mk(self):
         """
         Create two files, root.mk and root-deps.mk, the first containing
         convenience variables, and the other dependency definitions for a
--- a/python/mozbuild/mozbuild/frontend/context.py
+++ b/python/mozbuild/mozbuild/frontend/context.py
@@ -325,29 +325,35 @@ class PathMeta(type):
         if isinstance(context, Path):
             assert value is None
             value = context
             context = context.context
         else:
             assert isinstance(context, Context)
             if isinstance(value, Path):
                 context = value.context
-        if not issubclass(cls, (SourcePath, ObjDirPath)):
-            cls = ObjDirPath if value.startswith('!') else SourcePath
+        if not issubclass(cls, (SourcePath, ObjDirPath, AbsolutePath)):
+            if value.startswith('!'):
+                cls = ObjDirPath
+            elif value.startswith('%'):
+                cls = AbsolutePath
+            else:
+                cls = SourcePath
         return super(PathMeta, cls).__call__(context, value)
 
 class Path(ContextDerivedValue, unicode):
     """Stores and resolves a source path relative to a given context
 
     This class is used as a backing type for some of the sandbox variables.
     It expresses paths relative to a context. Supported paths are:
       - '/topsrcdir/relative/paths'
       - 'srcdir/relative/paths'
       - '!/topobjdir/relative/paths'
       - '!objdir/relative/paths'
+      - '%/filesystem/absolute/paths'
     """
     __metaclass__ = PathMeta
 
     def __new__(cls, context, value=None):
         return super(Path, cls).__new__(cls, value)
 
     def __init__(self, context, value=None):
         # Only subclasses should be instantiated.
@@ -393,16 +399,18 @@ class Path(ContextDerivedValue, unicode)
         return hash(self.full_path)
 
 
 class SourcePath(Path):
     """Like Path, but limited to paths in the source directory."""
     def __init__(self, context, value):
         if value.startswith('!'):
             raise ValueError('Object directory paths are not allowed')
+        if value.startswith('%'):
+            raise ValueError('Filesystem absolute paths are not allowed')
         super(SourcePath, self).__init__(context, value)
 
         if value.startswith('/'):
             path = None
             # If the path starts with a '/' and is actually relative to an
             # external source dir, use that as base instead of topsrcdir.
             if context.config.external_source_dir:
                 path = mozpath.join(context.config.external_source_dir,
@@ -424,26 +432,38 @@ class SourcePath(Path):
         """
         return ObjDirPath(self.context, '!%s' % self).full_path
 
 
 class ObjDirPath(Path):
     """Like Path, but limited to paths in the object directory."""
     def __init__(self, context, value=None):
         if not value.startswith('!'):
-            raise ValueError('Source paths are not allowed')
+            raise ValueError('Object directory paths must start with ! prefix')
         super(ObjDirPath, self).__init__(context, value)
 
         if value.startswith('!/'):
             path = mozpath.join(context.config.topobjdir,value[2:])
         else:
             path = mozpath.join(context.objdir, value[1:])
         self.full_path = mozpath.normpath(path)
 
 
+class AbsolutePath(Path):
+    """Like Path, but allows arbitrary paths outside the source and object directories."""
+    def __init__(self, context, value=None):
+        if not value.startswith('%'):
+            raise ValueError('Absolute paths must start with % prefix')
+        if not os.path.isabs(value[1:]):
+            raise ValueError('Path \'%s\' is not absolute' % value[1:])
+        super(AbsolutePath, self).__init__(context, value)
+
+        self.full_path = mozpath.normpath(value[1:])
+
+
 @memoize
 def ContextDerivedTypedList(klass, base_class=List):
     """Specialized TypedList for use with ContextDerivedValue types.
     """
     assert issubclass(klass, ContextDerivedValue)
     class _TypedList(ContextDerivedValue, TypedList(klass, base_class)):
         def __init__(self, context, iterable=[]):
             self.context = context
@@ -666,22 +686,22 @@ VARIABLES = {
         """Android resource files generated as part of the build.
 
         This variable contains a list of files that are expected to be
         generated (often by preprocessing) into a 'res' directory as
         part of the build process, and subsequently merged into an APK
         file.
         """, 'export'),
 
-    'ANDROID_RES_DIRS': (List, list,
+    'ANDROID_RES_DIRS': (ContextDerivedTypedListWithItems(Path, List), list,
         """Android resource directories.
 
-        This variable contains a list of directories, each relative to
-        the srcdir, containing static files to package into a 'res'
-        directory and merge into an APK file.
+        This variable contains a list of directories containing static
+        files to package into a 'res' directory and merge into an APK
+        file.
         """, 'export'),
 
     'ANDROID_ECLIPSE_PROJECT_TARGETS': (dict, dict,
         """Defines Android Eclipse project targets.
 
         This variable should not be populated directly. Instead, it should
         populated by calling add_android_eclipse{_library}_project().
         """, 'export'),
--- a/python/mozbuild/mozbuild/frontend/data.py
+++ b/python/mozbuild/mozbuild/frontend/data.py
@@ -958,8 +958,20 @@ class AndroidEclipseProjectData(object):
         cpe = ClassPathEntry()
         cpe.srcdir = srcdir
         cpe.dstdir = dstdir
         cpe.path = path
         cpe.exclude_patterns = list(exclude_patterns)
         cpe.ignore_warnings = ignore_warnings
         self._classpathentries.append(cpe)
         return cpe
+
+
+class AndroidResDirs(ContextDerived):
+    """Represents Android resource directories."""
+
+    __slots__ = (
+        'paths',
+    )
+
+    def __init__(self, context, paths):
+        ContextDerived.__init__(self, context)
+        self.paths = paths
--- a/python/mozbuild/mozbuild/frontend/emitter.py
+++ b/python/mozbuild/mozbuild/frontend/emitter.py
@@ -19,16 +19,17 @@ from mozbuild.util import (
 )
 
 import mozpack.path as mozpath
 import manifestparser
 import reftest
 import mozinfo
 
 from .data import (
+    AndroidResDirs,
     BrandingFiles,
     ConfigFileSubstitution,
     ContextWrapped,
     Defines,
     DistFiles,
     DirectoryTraversal,
     Exports,
     FinalTargetFiles,
@@ -72,17 +73,17 @@ from .data import (
     WebIDLFile,
     XPIDLFile,
 )
 
 from .reader import SandboxValidationError
 
 from .context import (
     Context,
-    ObjDirPath,
+    AbsolutePath,
     SourcePath,
     ObjDirPath,
     Path,
     SubContext,
     TemplateContext,
 )
 
 
@@ -550,17 +551,16 @@ class TreeMetadataEmitter(LoggingMixin):
                                          context);
 
         # Proxy some variables as-is until we have richer classes to represent
         # them. We should aim to keep this set small because it violates the
         # desired abstraction of the build definition away from makefiles.
         passthru = VariablePassthru(context)
         varlist = [
             'ANDROID_GENERATED_RESFILES',
-            'ANDROID_RES_DIRS',
             'DISABLE_STL_WRAPPING',
             'EXTRA_COMPONENTS',
             'EXTRA_DSO_LDOPTS',
             'EXTRA_PP_COMPONENTS',
             'FAIL_ON_WARNINGS',
             'USE_STATIC_LIBS',
             'PYTHON_UNIT_TESTS',
             'RCFILE',
@@ -695,16 +695,25 @@ class TreeMetadataEmitter(LoggingMixin):
             yield obj
 
         for name, jar in context.get('JAVA_JAR_TARGETS', {}).items():
             yield ContextWrapped(context, jar)
 
         for name, data in context.get('ANDROID_ECLIPSE_PROJECT_TARGETS', {}).items():
             yield ContextWrapped(context, data)
 
+        paths = context.get('ANDROID_RES_DIRS')
+        if paths:
+            for p in paths:
+                if isinstance(p, SourcePath) and not os.path.isdir(p.full_path):
+                    raise SandboxValidationError('Directory listed in '
+                        'ANDROID_RES_DIRS is not a directory: \'%s\'' %
+                            p.full_path, context)
+            yield AndroidResDirs(context, paths)
+
         if passthru.variables:
             yield passthru
 
     def _create_substitution(self, cls, context, path):
         if os.path.isabs(path):
             path = path[1:]
 
         sub = cls(context)
new file mode 100644
new file mode 100644
--- /dev/null
+++ b/python/mozbuild/mozbuild/test/frontend/data/android-res-dirs/moz.build
@@ -0,0 +1,9 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# Any copyright is dedicated to the Public Domain.
+# http://creativecommons.org/publicdomain/zero/1.0/
+
+ANDROID_RES_DIRS += [
+    '/dir1',
+    '!/dir2',
+    '%/dir3',
+]
--- a/python/mozbuild/mozbuild/test/frontend/test_context.py
+++ b/python/mozbuild/mozbuild/test/frontend/test_context.py
@@ -3,16 +3,17 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 import os
 import unittest
 
 from mozunit import main
 
 from mozbuild.frontend.context import (
+    AbsolutePath,
     Context,
     ContextDerivedTypedList,
     ContextDerivedTypedListWithItems,
     Files,
     FUNCTIONS,
     ObjDirPath,
     Path,
     SourcePath,
@@ -449,16 +450,28 @@ class TestPaths(unittest.TestCase):
         self.assertIsInstance(path, ObjDirPath)
         self.assertEqual(path, '!/qux/qux')
         self.assertEqual(path.full_path,
                          mozpath.join(config.topobjdir, 'qux', 'qux'))
 
         path = Path(path)
         self.assertIsInstance(path, ObjDirPath)
 
+    def test_absolute_path(self):
+        config = self.config
+        ctxt = Context(config=config)
+        ctxt.push_source(mozpath.join(config.topsrcdir, 'foo', 'moz.build'))
+
+        path = AbsolutePath(ctxt, '%/qux')
+        self.assertEqual(path, '%/qux')
+        self.assertEqual(path.full_path, '/qux')
+
+        with self.assertRaises(ValueError):
+            path = AbsolutePath(ctxt, '%qux')
+
     def test_path_with_mixed_contexts(self):
         config = self.config
         ctxt1 = Context(config=config)
         ctxt1.push_source(mozpath.join(config.topsrcdir, 'foo', 'moz.build'))
         ctxt2 = Context(config=config)
         ctxt2.push_source(mozpath.join(config.topsrcdir, 'bar', 'moz.build'))
 
         path1 = Path(ctxt1, 'qux')
--- a/python/mozbuild/mozbuild/test/frontend/test_emitter.py
+++ b/python/mozbuild/mozbuild/test/frontend/test_emitter.py
@@ -5,16 +5,17 @@
 from __future__ import unicode_literals
 
 import os
 import unittest
 
 from mozunit import main
 
 from mozbuild.frontend.data import (
+    AndroidResDirs,
     BrandingFiles,
     ConfigFileSubstitution,
     Defines,
     DistFiles,
     DirectoryTraversal,
     Exports,
     GeneratedFile,
     GeneratedInclude,
@@ -869,10 +870,26 @@ class TestEmitterBasic(unittest.TestCase
 
     def test_missing_dist_files(self):
         """Test that DIST_FILES with missing files throws errors."""
         with self.assertRaisesRegexp(SandboxValidationError, 'File listed in '
             'DIST_FILES does not exist'):
             reader = self.reader('dist-files-missing')
             self.read_topsrcdir(reader)
 
+    def test_android_res_dirs(self):
+        """Test that ANDROID_RES_DIRS works properly."""
+        reader = self.reader('android-res-dirs')
+        objs = self.read_topsrcdir(reader)
+
+        self.assertEqual(len(objs), 1)
+        self.assertIsInstance(objs[0], AndroidResDirs)
+
+        # Android resource directories are ordered.
+        expected = [
+            mozpath.join(reader.config.topsrcdir, 'dir1'),
+            mozpath.join(reader.config.topobjdir, 'dir2'),
+            '/dir3',
+        ]
+        self.assertEquals([p.full_path for p in objs[0].paths], expected)
+
 if __name__ == '__main__':
     main()