Bug 900526, part 3: Define FINAL_TARGET and friends in moz.build, r=gps
authorJoshua Cranmer <Pidgeot18@gmail.com>
Mon, 21 Oct 2013 13:08:35 -0500
changeset 160227 6916000f7b8ce028107d3c078148bfe2c19e008e
parent 160226 fb42a764f90bc91a628e472889e00b27b28b55ed
child 160228 5452527770cb322cb7f50e8abedccf2eb6c8a4a0
push idunknown
push userunknown
push dateunknown
reviewersgps
bugs900526
milestone27.0a1
Bug 900526, part 3: Define FINAL_TARGET and friends in moz.build, r=gps
config/config.mk
js/src/config/config.mk
python/mozbuild/mozbuild/backend/recursivemake.py
python/mozbuild/mozbuild/frontend/data.py
python/mozbuild/mozbuild/frontend/emitter.py
python/mozbuild/mozbuild/frontend/sandbox.py
python/mozbuild/mozbuild/frontend/sandbox_symbols.py
python/mozbuild/mozbuild/test/backend/common.py
python/mozbuild/mozbuild/test/backend/data/final_target/both/moz.build
python/mozbuild/mozbuild/test/backend/data/final_target/dist-subdir/moz.build
python/mozbuild/mozbuild/test/backend/data/final_target/final-target/moz.build
python/mozbuild/mozbuild/test/backend/data/final_target/moz.build
python/mozbuild/mozbuild/test/backend/data/final_target/xpi-name/moz.build
python/mozbuild/mozbuild/test/backend/test_recursivemake.py
--- a/config/config.mk
+++ b/config/config.mk
@@ -144,22 +144,26 @@ include $(topsrcdir)/config/makefiles/fu
 endif
 
 RM = rm -f
 
 # LIBXUL_DIST is not defined under js/src, thus we make it mean DIST there.
 LIBXUL_DIST ?= $(DIST)
 
 # FINAL_TARGET specifies the location into which we copy end-user-shipped
-# build products (typelibs, components, chrome).
+# build products (typelibs, components, chrome). It may already be specified by
+# a moz.build file.
 #
 # If XPI_NAME is set, the files will be shipped to $(DIST)/xpi-stage/$(XPI_NAME)
 # instead of $(DIST)/bin. In both cases, if DIST_SUBDIR is set, the files will be
 # shipped to a $(DIST_SUBDIR) subdirectory.
-FINAL_TARGET = $(if $(XPI_NAME),$(DIST)/xpi-stage/$(XPI_NAME),$(DIST)/bin)$(DIST_SUBDIR:%=/%)
+FINAL_TARGET ?= $(if $(XPI_NAME),$(DIST)/xpi-stage/$(XPI_NAME),$(DIST)/bin)$(DIST_SUBDIR:%=/%)
+# Override the stored value for the check to make sure that the variable is not
+# redefined in the Makefile.in value.
+FINAL_TARGET_FROZEN := '$(FINAL_TARGET)'
 
 ifdef XPI_NAME
 DEFINES += -DXPI_NAME=$(XPI_NAME)
 endif
 
 # The VERSION_NUMBER is suffixed onto the end of the DLLs we ship.
 VERSION_NUMBER		= 50
 
--- a/js/src/config/config.mk
+++ b/js/src/config/config.mk
@@ -144,22 +144,26 @@ include $(topsrcdir)/config/makefiles/fu
 endif
 
 RM = rm -f
 
 # LIBXUL_DIST is not defined under js/src, thus we make it mean DIST there.
 LIBXUL_DIST ?= $(DIST)
 
 # FINAL_TARGET specifies the location into which we copy end-user-shipped
-# build products (typelibs, components, chrome).
+# build products (typelibs, components, chrome). It may already be specified by
+# a moz.build file.
 #
 # If XPI_NAME is set, the files will be shipped to $(DIST)/xpi-stage/$(XPI_NAME)
 # instead of $(DIST)/bin. In both cases, if DIST_SUBDIR is set, the files will be
 # shipped to a $(DIST_SUBDIR) subdirectory.
-FINAL_TARGET = $(if $(XPI_NAME),$(DIST)/xpi-stage/$(XPI_NAME),$(DIST)/bin)$(DIST_SUBDIR:%=/%)
+FINAL_TARGET ?= $(if $(XPI_NAME),$(DIST)/xpi-stage/$(XPI_NAME),$(DIST)/bin)$(DIST_SUBDIR:%=/%)
+# Override the stored value for the check to make sure that the variable is not
+# redefined in the Makefile.in value.
+FINAL_TARGET_FROZEN := '$(FINAL_TARGET)'
 
 ifdef XPI_NAME
 DEFINES += -DXPI_NAME=$(XPI_NAME)
 endif
 
 # The VERSION_NUMBER is suffixed onto the end of the DLLs we ship.
 VERSION_NUMBER		= 50
 
--- a/python/mozbuild/mozbuild/backend/recursivemake.py
+++ b/python/mozbuild/mozbuild/backend/recursivemake.py
@@ -23,16 +23,17 @@ import mozpack.path as mozpath
 from .common import CommonBackend
 from ..frontend.data import (
     ConfigFileSubstitution,
     Defines,
     DirectoryTraversal,
     Exports,
     GeneratedEventWebIDLFile,
     GeneratedWebIDLFile,
+    InstallationTarget,
     IPDLFile,
     LocalInclude,
     PreprocessedTestWebIDLFile,
     PreprocessedWebIDLFile,
     Program,
     SandboxDerived,
     TestWebIDLFile,
     VariablePassthru,
@@ -404,16 +405,19 @@ class RecursiveMakeBackend(CommonBackend
             self._process_program(obj.program, backend_file)
 
         elif isinstance(obj, TestManifest):
             self._process_test_manifest(obj, backend_file)
 
         elif isinstance(obj, LocalInclude):
             self._process_local_include(obj.path, backend_file)
 
+        elif isinstance(obj, InstallationTarget):
+            self._process_installation_target(obj, backend_file)
+
         self._backend_files[obj.srcdir] = backend_file
 
     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
         hopefully proper directory traversal.
         """
@@ -868,16 +872,32 @@ class RecursiveMakeBackend(CommonBackend
             if not os.path.exists(source):
                 raise Exception('File listed in EXPORTS does not exist: %s' % source)
 
         children = exports.get_children()
         for subdir in sorted(children):
             self._process_exports(obj, children[subdir], backend_file,
                 namespace=namespace + subdir)
 
+    def _process_installation_target(self, obj, backend_file):
+        # A few makefiles need to be able to override the following rules via
+        # make XPI_NAME=blah commands, so we default to the lazy evaluation as
+        # much as possible here to avoid breaking things.
+        if obj.xpiname:
+            backend_file.write('XPI_NAME = %s\n' % (obj.xpiname))
+        if obj.subdir:
+            backend_file.write('DIST_SUBDIR = %s\n' % (obj.subdir))
+        if obj.target and not obj.is_custom():
+            backend_file.write('FINAL_TARGET = $(DEPTH)/%s\n' % (obj.target))
+        else:
+            backend_file.write('FINAL_TARGET = $(if $(XPI_NAME),$(DIST)/xpi-stage/$(XPI_NAME),$(DIST)/bin)$(DIST_SUBDIR:%=/%)\n')
+
+        if not obj.enabled:
+            backend_file.write('NO_DIST_INSTALL := 1\n')
+
     def _handle_idl_manager(self, manager):
         build_files = self._install_manifests['xpidl']
 
         for p in ('Makefile', 'backend.mk', '.deps/.mkdir.done',
             'xpt/.mkdir.done'):
             build_files.add_optional_exists(p)
 
         for idl in manager.idls.values():
--- a/python/mozbuild/mozbuild/frontend/data.py
+++ b/python/mozbuild/mozbuild/frontend/data.py
@@ -15,16 +15,17 @@ contains the code for converting execute
 structures.
 """
 
 from __future__ import unicode_literals
 
 import os
 
 from collections import OrderedDict
+from .sandbox_symbols import compute_final_target
 
 
 class TreeMetadata(object):
     """Base class for all data being captured."""
 
 
 class ReaderSummary(TreeMetadata):
     """A summary of what the reader did."""
@@ -348,8 +349,34 @@ class LocalInclude(SandboxDerived):
     __slots__ = (
         'path',
     )
 
     def __init__(self, sandbox, path):
         SandboxDerived.__init__(self, sandbox)
 
         self.path = path
+
+class InstallationTarget(SandboxDerived):
+    """Describes the rules that affect where files get installed to."""
+
+    __slots__ = (
+        'xpiname',
+        'subdir',
+        'target',
+        'enabled'
+    )
+
+    def __init__(self, sandbox):
+        SandboxDerived.__init__(self, sandbox)
+
+        self.xpiname = sandbox.get('XPI_NAME', '')
+        self.subdir = sandbox.get('DIST_SUBDIR', '')
+        self.target = sandbox['FINAL_TARGET']
+        self.enabled = not sandbox.get('NO_DIST_INSTALL', False)
+
+    def is_custom(self):
+        """Returns whether or not the target is not derived from the default
+        given xpiname and subdir."""
+
+        return compute_final_target(dict(
+            XPI_NAME=self.xpiname,
+            DIST_SUBDIR=self.subdir)) == self.target
--- a/python/mozbuild/mozbuild/frontend/emitter.py
+++ b/python/mozbuild/mozbuild/frontend/emitter.py
@@ -19,16 +19,17 @@ from mozpack.files import FileFinder
 
 from .data import (
     ConfigFileSubstitution,
     Defines,
     DirectoryTraversal,
     Exports,
     GeneratedEventWebIDLFile,
     GeneratedWebIDLFile,
+    InstallationTarget,
     IPDLFile,
     LocalInclude,
     PreprocessedTestWebIDLFile,
     PreprocessedWebIDLFile,
     Program,
     ReaderSummary,
     TestWebIDLFile,
     TestManifest,
@@ -203,16 +204,20 @@ class TreeMetadataEmitter(LoggingMixin):
             ('PREPROCESSED_WEBIDL_FILES', PreprocessedWebIDLFile),
             ('TEST_WEBIDL_FILES', TestWebIDLFile),
             ('WEBIDL_FILES', WebIDLFile),
         ]
         for sandbox_var, klass in simple_lists:
             for name in sandbox.get(sandbox_var, []):
                 yield klass(sandbox, name)
 
+        if sandbox.get('FINAL_TARGET') or sandbox.get('XPI_NAME') or \
+                sandbox.get('DIST_SUBDIR'):
+            yield InstallationTarget(sandbox)
+
         # While there are multiple test manifests, the behavior is very similar
         # across them. We enforce this by having common handling of all
         # manifests and outputting a single class type with the differences
         # described inside the instance.
         #
         # Keys are variable prefixes and values are tuples describing how these
         # manifests should be handled:
         #
--- a/python/mozbuild/mozbuild/frontend/sandbox.py
+++ b/python/mozbuild/mozbuild/frontend/sandbox.py
@@ -109,17 +109,24 @@ class GlobalNamespace(dict):
             pass
 
         # The variable isn't present yet. Fall back to VARIABLES.
         default = self._allowed_variables.get(name, None)
         if default is None:
             self.last_name_error = KeyError('global_ns', 'get_unknown', name)
             raise self.last_name_error
 
-        dict.__setitem__(self, name, copy.deepcopy(default[2]))
+        # If the default is specifically a lambda (or, rather, any function--but
+        # not a class that can be called), then it is actually a rule to
+        # generate the default that should be used.
+        default_rule = default[2]
+        if isinstance(default_rule, type(lambda: None)):
+            default_rule = default_rule(self)
+
+        dict.__setitem__(self, name, copy.deepcopy(default_rule))
         return dict.__getitem__(self, name)
 
     def __setitem__(self, name, value):
         if self._allow_all_writes:
             dict.__setitem__(self, name, value)
             return
 
         # We don't need to check for name.isupper() here because LocalNamespace
--- a/python/mozbuild/mozbuild/frontend/sandbox_symbols.py
+++ b/python/mozbuild/mozbuild/frontend/sandbox_symbols.py
@@ -19,16 +19,28 @@ from __future__ import unicode_literals
 
 from collections import OrderedDict
 from mozbuild.util import (
     HierarchicalStringList,
     StrictOrderingOnAppendList,
 )
 
 
+def compute_final_target(variables):
+    """Convert the default value for FINAL_TARGET"""
+    basedir = 'dist/'
+    if variables['XPI_NAME']:
+        basedir += 'xpi-stage/' + variables['XPI_NAME']
+    else:
+        basedir += 'bin'
+    if variables['DIST_SUBDIR']:
+        basedir += '/' + variables['DIST_SUBDIR']
+    return basedir
+ 
+ 
 # This defines the set of mutable global variables.
 #
 # Each variable is a tuple of:
 #
 #   (storage_type, input_types, default_value, docs, tier)
 #
 # Tier says for which specific tier the variable has an effect.
 # Valid tiers are:
@@ -493,16 +505,42 @@ VARIABLES = {
 
     'WEBRTC_SIGNALLING_TEST_MANIFESTS': (StrictOrderingOnAppendList, list, [],
         """List of manifest files defining WebRTC signalling tests.
         """, None),
 
     'XPCSHELL_TESTS_MANIFESTS': (StrictOrderingOnAppendList, list, [],
         """List of manifest files defining xpcshell tests.
         """, None),
+
+    # The following variables are used to control the target of installed files.
+    'XPI_NAME': (unicode, unicode, "",
+        """The name of an extension XPI to generate.
+
+        When this variable is present, the results of this directory will end up
+        being packaged into an extension instead of the main dist/bin results.
+        """, 'libs'),
+
+    'DIST_SUBDIR': (unicode, unicode, "",
+        """The name of an alternate directory to install files to.
+
+        When this variable is present, the results of this directory will end up
+        being placed in the $(DIST_SUBDIR) subdirectory of where it would
+        otherwise be placed.
+        """, 'libs'),
+
+    'FINAL_TARGET': (unicode, unicode, compute_final_target,
+        """The name of the directory to install targets to.
+
+        The directory is relative to the top of the object directory. The
+        default value is dependent on the values of XPI_NAME and DIST_SUBDIR. If
+        neither are present, the result is dist/bin. If XPI_NAME is present, the
+        result is dist/xpi-stage/$(XPI_NAME). If DIST_SUBDIR is present, then
+        the $(DIST_SUBDIR) directory of the otherwise default value is used.
+        """, 'libs'),
 }
 
 # The set of functions exposed to the sandbox.
 #
 # Each entry is a tuple of:
 #
 #  (method attribute, (argument types), docs)
 #
--- a/python/mozbuild/mozbuild/test/backend/common.py
+++ b/python/mozbuild/mozbuild/test/backend/common.py
@@ -83,16 +83,21 @@ CONFIGS = {
         'non_global_defines': [],
         'substs': [],
     },
     'local_includes': {
         'defines': [],
         'non_global_defines': [],
         'substs': [],
     },
+    'final_target': {
+        'defines': [],
+        'non_global_defines': [],
+        'substs': [],
+    },
 }
 
 
 class BackendTester(unittest.TestCase):
     def _get_environment(self, name):
         """Obtain a new instance of a ConfigEnvironment for a known profile.
 
         A new temporary object directory is created for the environment. The
new file mode 100644
--- /dev/null
+++ b/python/mozbuild/mozbuild/test/backend/data/final_target/both/moz.build
@@ -0,0 +1,6 @@
+# -*- 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/
+
+XPI_NAME = 'mycrazyxpi'
+DIST_SUBDIR = 'asubdir'
new file mode 100644
--- /dev/null
+++ b/python/mozbuild/mozbuild/test/backend/data/final_target/dist-subdir/moz.build
@@ -0,0 +1,5 @@
+# -*- 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/
+
+DIST_SUBDIR = 'asubdir'
new file mode 100644
--- /dev/null
+++ b/python/mozbuild/mozbuild/test/backend/data/final_target/final-target/moz.build
@@ -0,0 +1,5 @@
+# -*- 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/
+
+FINAL_TARGET = 'random-final-target'
new file mode 100644
--- /dev/null
+++ b/python/mozbuild/mozbuild/test/backend/data/final_target/moz.build
@@ -0,0 +1,5 @@
+# -*- 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/
+
+DIRS += ['xpi-name', 'dist-subdir', 'both', 'final-target']
new file mode 100644
--- /dev/null
+++ b/python/mozbuild/mozbuild/test/backend/data/final_target/xpi-name/moz.build
@@ -0,0 +1,5 @@
+# -*- 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/
+
+XPI_NAME = 'mycrazyxpi'
--- a/python/mozbuild/mozbuild/test/backend/test_recursivemake.py
+++ b/python/mozbuild/mozbuild/test/backend/test_recursivemake.py
@@ -500,10 +500,43 @@ class TestRecursiveMakeBackend(BackendTe
         expected = [
             'LOCAL_INCLUDES += -I$(topsrcdir)/bar/baz',
             'LOCAL_INCLUDES += -I$(srcdir)/foo',
         ]
 
         found = [str for str in lines if str.startswith('LOCAL_INCLUDES')]
         self.assertEqual(found, expected)
 
+    def test_final_target(self):
+        """Test that FINAL_TARGET is written to backend.mk correctly."""
+        env = self._consume('final_target', RecursiveMakeBackend)
+
+        final_target_rule = "FINAL_TARGET = $(if $(XPI_NAME),$(DIST)/xpi-stage/$(XPI_NAME),$(DIST)/bin)$(DIST_SUBDIR:%=/%)"
+        print([x for x in os.walk(env.topobjdir)])
+        expected = dict()
+        expected[env.topobjdir] = []
+        expected[os.path.join(env.topobjdir, 'both')] = [
+            'XPI_NAME = mycrazyxpi',
+            'DIST_SUBDIR = asubdir',
+            final_target_rule
+        ]
+        expected[os.path.join(env.topobjdir, 'dist-subdir')] = [
+            'DIST_SUBDIR = asubdir',
+            final_target_rule
+        ]
+        expected[os.path.join(env.topobjdir, 'xpi-name')] = [
+            'XPI_NAME = mycrazyxpi',
+            final_target_rule
+        ]
+        expected[os.path.join(env.topobjdir, 'final-target')] = [
+            'FINAL_TARGET = $(DEPTH)/random-final-target'
+        ]
+        for key, expected_rules in expected.iteritems():
+            backend_path = os.path.join(key, 'backend.mk')
+            lines = [l.strip() for l in open(backend_path, 'rt').readlines()[2:]]
+            found = [str for str in lines if
+                str.startswith('FINAL_TARGET') or str.startswith('XPI_NAME') or
+                str.startswith('DIST_SUBDIR')]
+            self.assertEqual(found, expected_rules)
+
+
 if __name__ == '__main__':
     main()