Re-land r1745: "Add an option to prune targets"

BUG=
R=mark@chromium.org

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

git-svn-id: http://gyp.googlecode.com/svn/trunk@1772 78cadc50-ecff-11dd-a971-7dbc132099af
diff --git a/pylib/gyp/__init__.py b/pylib/gyp/__init__.py
index 8e82fb4..f3de408 100755
--- a/pylib/gyp/__init__.py
+++ b/pylib/gyp/__init__.py
@@ -126,7 +126,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):
@@ -332,6 +332,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
@@ -492,7 +495,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 d371b27..a08cce9 100644
--- a/pylib/gyp/input.py
+++ b/pylib/gyp/input.py
@@ -2560,6 +2560,41 @@
       TurnIntIntoStrInList(item)
 
 
+def PruneUnwantedTargets(targets, flat_list, dependency_nodes, root_targets,
+                         data):
+  """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]
+
+  # Prune unwanted targets from each build_file's data dict.
+  for build_file in data['target_build_files']:
+    if not 'targets' in data[build_file]:
+      continue
+    new_targets = []
+    for target in data[build_file]['targets']:
+      qualified_name = gyp.common.QualifiedTarget(build_file,
+                                                  target['target_name'],
+                                                  target['toolset'])
+      if qualified_name in wanted_targets:
+        new_targets.append(target)
+    data[build_file]['targets'] = new_targets
+
+  return wanted_targets, wanted_flat_list
+
+
 def VerifyNoCollidingTargets(targets):
   """Verify that no two targets in the same directory share the same name.
 
@@ -2588,7 +2623,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
@@ -2673,6 +2708,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, data)
+
   # 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..99d7a9a
--- /dev/null
+++ b/test/prune_targets/gyptest-prune-targets.py
@@ -0,0 +1,63 @@
+#!/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()
+
+build_error_code = {
+  'android': 2,
+  'make': 2,
+  'msvs': 1,
+  'ninja': 1,
+  'xcode': 65,
+}[test.format]
+
+# By default, everything will be included.
+test.run_gyp('test1.gyp')
+test.build('test2.gyp', 'lib1')
+test.build('test2.gyp', 'lib2')
+test.build('test2.gyp', 'lib3')
+test.build('test2.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('test2.gyp', 'lib1')
+test.build('test2.gyp', 'lib2', status=build_error_code, stderr=None)
+test.build('test2.gyp', 'lib3', status=build_error_code, stderr=None)
+test.build('test2.gyp', 'lib_indirect')
+test.build('test1.gyp', 'program1')
+test.build('test1.gyp', 'program2', status=build_error_code, stderr=None)
+test.build('test1.gyp', 'program3', status=build_error_code, stderr=None)
+
+# With deep dependencies of program2 only.
+test.run_gyp('test1.gyp', '--root-target=program2')
+test.build('test2.gyp', 'lib1', status=build_error_code, stderr=None)
+test.build('test2.gyp', 'lib2')
+test.build('test2.gyp', 'lib3', status=build_error_code, stderr=None)
+test.build('test2.gyp', 'lib_indirect')
+test.build('test1.gyp', 'program1', status=build_error_code, stderr=None)
+test.build('test1.gyp', 'program2')
+test.build('test1.gyp', 'program3', status=build_error_code, stderr=None)
+
+# With deep dependencies of program1 and program2.
+test.run_gyp('test1.gyp', '--root-target=program1', '--root-target=program2')
+test.build('test2.gyp', 'lib1')
+test.build('test2.gyp', 'lib2')
+test.build('test2.gyp', 'lib3', status=build_error_code, stderr=None)
+test.build('test2.gyp', 'lib_indirect')
+test.build('test1.gyp', 'program1')
+test.build('test1.gyp', 'program2')
+test.build('test1.gyp', 'program3', status=build_error_code, 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' ],
+    },
+  ],
+}