Add an option to prune targets

Normally, when a target in file1.gyp depends on a target in file2.gyp,
gyp will generate projects for all targets in file2.gyp, even if the
target in file1.gyp does not depend on all of them.

This patch adds a '--root-target' command-line option, which allows users
to specify a list of "root" targets in the dependency tree.  When this
list is provided, gyp will only generate projects for targets that
are deep dependencies of these root targets.

As an example, when used in the chromium repository, running 'gyp_chromium'
with 'content.gyp' generates 397 subninja files.  With '--root-target=content'
specified, this is reduced to just 260 subninja files.

BUG=
R=mark@chromium.org

Review URL: https://codereview.chromium.org/24803004

git-svn-id: http://gyp.googlecode.com/svn/trunk@1745 78cadc50-ecff-11dd-a971-7dbc132099af
diff --git a/pylib/gyp/__init__.py b/pylib/gyp/__init__.py
index 61ce418..66d5cec 100755
--- a/pylib/gyp/__init__.py
+++ b/pylib/gyp/__init__.py
@@ -128,7 +128,7 @@
   # Process the input specific to this generator.
   result = gyp.input.Load(build_files, default_variables, includes[:],
                           depth, generator_input_info, check, circular_check,
-                          params['parallel'])
+                          params['parallel'], params['root_targets'])
   return [generator] + result
 
 def NameValueListToDict(name_value_list):
@@ -337,6 +337,9 @@
   parser.add_option('--toplevel-dir', dest='toplevel_dir', action='store',
                     default=None, metavar='DIR', type='path',
                     help='directory to use as the root of the source tree')
+  parser.add_option('-R', '--root-target', dest='root_targets',
+                    action='append', metavar='TARGET',
+                    help='include only TARGET and its deep dependencies')
 
   options, build_files_arg = parser.parse_args(args)
   build_files = build_files_arg
@@ -506,7 +509,8 @@
               'build_files_arg': build_files_arg,
               'gyp_binary': sys.argv[0],
               'home_dot_gyp': home_dot_gyp,
-              'parallel': options.parallel}
+              'parallel': options.parallel,
+              'root_targets': options.root_targets}
 
     # Start with the default variables from the command line.
     [generator, flat_list, targets, data] = Load(build_files, format,
diff --git a/pylib/gyp/common.py b/pylib/gyp/common.py
index 880bc68..b9d2abe 100644
--- a/pylib/gyp/common.py
+++ b/pylib/gyp/common.py
@@ -44,6 +44,14 @@
     e.args = (str(e.args[0]) + ' ' + msg,) + e.args[1:]
 
 
+def FindQualifiedTargets(target, qualified_list):
+  """
+  Given a list of qualified targets, return the qualified targets for the
+  specified |target|.
+  """
+  return [t for t in qualified_list if ParseQualifiedTarget(t)[1] == target]
+
+
 def ParseQualifiedTarget(target):
   # Splits a qualified target into a build file, target name and toolset.
 
diff --git a/pylib/gyp/input.py b/pylib/gyp/input.py
index 8abfa3d..b151d24 100644
--- a/pylib/gyp/input.py
+++ b/pylib/gyp/input.py
@@ -2562,6 +2562,26 @@
       TurnIntIntoStrInList(item)
 
 
+def PruneUnwantedTargets(targets, flat_list, dependency_nodes, root_targets):
+  """Return only the targets that are deep dependencies of |root_targets|."""
+  qualified_root_targets = []
+  for target in root_targets:
+    target = target.strip()
+    qualified_targets = gyp.common.FindQualifiedTargets(target, flat_list)
+    if not qualified_targets:
+      raise GypError("Could not find target %s" % target)
+    qualified_root_targets.extend(qualified_targets)
+
+  wanted_targets = {}
+  for target in qualified_root_targets:
+    wanted_targets[target] = targets[target]
+    for dependency in dependency_nodes[target].DeepDependencies():
+      wanted_targets[dependency] = targets[dependency]
+
+  wanted_flat_list = [t for t in flat_list if t in wanted_targets]
+  return wanted_targets, wanted_flat_list
+
+
 def VerifyNoCollidingTargets(targets):
   """Verify that no two targets in the same directory share the same name.
 
@@ -2590,7 +2610,7 @@
 
 
 def Load(build_files, variables, includes, depth, generator_input_info, check,
-         circular_check, parallel):
+         circular_check, parallel, root_targets):
   # Set up path_sections and non_configuration_keys with the default data plus
   # the generator-specifc data.
   global path_sections
@@ -2678,6 +2698,12 @@
 
   [dependency_nodes, flat_list] = BuildDependencyList(targets)
 
+  if root_targets:
+    # Remove, from |targets| and |flat_list|, the targets that are not deep
+    # dependencies of the targets specified in |root_targets|.
+    targets, flat_list = PruneUnwantedTargets(
+        targets, flat_list, dependency_nodes, root_targets)
+
   # Check that no two targets in the same directory have the same name.
   VerifyNoCollidingTargets(flat_list)
 
diff --git a/test/prune_targets/gyptest-prune-targets.py b/test/prune_targets/gyptest-prune-targets.py
new file mode 100644
index 0000000..6526975
--- /dev/null
+++ b/test/prune_targets/gyptest-prune-targets.py
@@ -0,0 +1,55 @@
+#!/usr/bin/env python
+
+# Copyright (c) 2013 Google Inc. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""
+Verifies --root-target removes the unnecessary targets.
+"""
+
+import TestGyp
+
+test = TestGyp.TestGyp()
+
+# By default, everything will be included.
+test.run_gyp('test1.gyp')
+test.build('test1.gyp', 'lib1')
+test.build('test1.gyp', 'lib2')
+test.build('test1.gyp', 'lib3')
+test.build('test1.gyp', 'lib_indirect')
+test.build('test1.gyp', 'program1')
+test.build('test1.gyp', 'program2')
+test.build('test1.gyp', 'program3')
+
+# With deep dependencies of program1 only.
+test.run_gyp('test1.gyp', '--root-target=program1')
+test.build('test1.gyp', 'lib1')
+test.build('test1.gyp', 'lib2', status=[1,2], stderr=None)
+test.build('test1.gyp', 'lib3', status=[1,2], stderr=None)
+test.build('test1.gyp', 'lib_indirect')
+test.build('test1.gyp', 'program1')
+test.build('test1.gyp', 'program2', status=[1,2], stderr=None)
+test.build('test1.gyp', 'program3', status=[1,2], stderr=None)
+
+# With deep dependencies of program2 only.
+test.run_gyp('test1.gyp', '--root-target=program2')
+test.build('test1.gyp', 'lib1', status=[1,2], stderr=None)
+test.build('test1.gyp', 'lib2')
+test.build('test1.gyp', 'lib3', status=[1,2], stderr=None)
+test.build('test1.gyp', 'lib_indirect')
+test.build('test1.gyp', 'program1', status=[1,2], stderr=None)
+test.build('test1.gyp', 'program2')
+test.build('test1.gyp', 'program3', status=[1,2], stderr=None)
+
+# With deep dependencies of program1 and program2.
+test.run_gyp('test1.gyp', '--root-target=program1', '--root-target=program2')
+test.build('test1.gyp', 'lib1')
+test.build('test1.gyp', 'lib2')
+test.build('test1.gyp', 'lib3', status=[1,2], stderr=None)
+test.build('test1.gyp', 'lib_indirect')
+test.build('test1.gyp', 'program1')
+test.build('test1.gyp', 'program2')
+test.build('test1.gyp', 'program3', status=[1,2], stderr=None)
+
+test.pass_test()
diff --git a/test/prune_targets/lib1.cc b/test/prune_targets/lib1.cc
new file mode 100644
index 0000000..692b7de
--- /dev/null
+++ b/test/prune_targets/lib1.cc
@@ -0,0 +1,6 @@
+// Copyright (c) 2013 Google Inc. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+void libfunc1() {
+}
diff --git a/test/prune_targets/lib2.cc b/test/prune_targets/lib2.cc
new file mode 100644
index 0000000..aed394a
--- /dev/null
+++ b/test/prune_targets/lib2.cc
@@ -0,0 +1,6 @@
+// Copyright (c) 2013 Google Inc. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+void libfunc2() {
+}
diff --git a/test/prune_targets/lib3.cc b/test/prune_targets/lib3.cc
new file mode 100644
index 0000000..af0f717
--- /dev/null
+++ b/test/prune_targets/lib3.cc
@@ -0,0 +1,6 @@
+// Copyright (c) 2013 Google Inc. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+void libfunc3() {
+}
diff --git a/test/prune_targets/lib_indirect.cc b/test/prune_targets/lib_indirect.cc
new file mode 100644
index 0000000..92d9ea4
--- /dev/null
+++ b/test/prune_targets/lib_indirect.cc
@@ -0,0 +1,6 @@
+// Copyright (c) 2013 Google Inc. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+void libfunc_indirect() {
+}
diff --git a/test/prune_targets/program.cc b/test/prune_targets/program.cc
new file mode 100644
index 0000000..c9ac070
--- /dev/null
+++ b/test/prune_targets/program.cc
@@ -0,0 +1,7 @@
+// Copyright (c) 2013 Google Inc. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+int main() {
+  return 0;
+}
diff --git a/test/prune_targets/test1.gyp b/test/prune_targets/test1.gyp
new file mode 100644
index 0000000..b65ec19
--- /dev/null
+++ b/test/prune_targets/test1.gyp
@@ -0,0 +1,26 @@
+# Copyright (c) 2013 Google Inc. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+{
+  'targets': [
+    {
+      'target_name': 'program1',
+      'type': 'executable',
+      'sources': [ 'program.cc' ],
+      'dependencies': [ 'test2.gyp:lib1' ],
+    },
+    {
+      'target_name': 'program2',
+      'type': 'executable',
+      'sources': [ 'program.cc' ],
+      'dependencies': [ 'test2.gyp:lib2' ],
+    },
+    {
+      'target_name': 'program3',
+      'type': 'executable',
+      'sources': [ 'program.cc' ],
+      'dependencies': [ 'test2.gyp:lib3' ],
+    },
+  ],
+}
diff --git a/test/prune_targets/test2.gyp b/test/prune_targets/test2.gyp
new file mode 100644
index 0000000..16f0fd3
--- /dev/null
+++ b/test/prune_targets/test2.gyp
@@ -0,0 +1,30 @@
+# Copyright (c) 2013 Google Inc. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+{
+  'targets': [
+    {
+      'target_name': 'lib1',
+      'type': 'static_library',
+      'sources': [ 'lib1.cc' ],
+      'dependencies': [ 'lib_indirect' ],
+    },
+    {
+      'target_name': 'lib2',
+      'type': 'static_library',
+      'sources': [ 'lib2.cc' ],
+      'dependencies': [ 'lib_indirect' ],
+    },
+    {
+      'target_name': 'lib3',
+      'type': 'static_library',
+      'sources': [ 'lib3.cc' ],
+    },
+    {
+      'target_name': 'lib_indirect',
+      'type': 'static_library',
+      'sources': [ 'lib_indirect.cc' ],
+    },
+  ],
+}