Repository: pantsbuild/pants

Issue:
Fix custom javac test
#5011 introduced a failing test in master, which was skipped in order to land #5016 and prevent further breakage:
https://github.com/pantsbuild/pants/blob/39bbec64cca103780ec68195a034d6ae205afcbb/tests/python/pants_test/backend/jvm/tasks/jvm_compile/java/test_zinc_compile_integration.py#L202

I'll work to fix this this evening.


Discussion:

Resolving patch:
diff --git a/src/python/pants/backend/jvm/subsystems/java.py b/src/python/pants/backend/jvm/subsystems/java.py
--- a/src/python/pants/backend/jvm/subsystems/java.py
+++ b/src/python/pants/backend/jvm/subsystems/java.py
@@ -45,17 +45,22 @@ def register_options(cls, register):
              fingerprint=True)
 
   def injectables(self, build_graph):
-    # If a javac target has been defined on disk, use it: otherwise, inject a ToolsJar
-    # target to provide the dependency.
-    javac_address = Address.parse(self._javac_spec)
-    if not build_graph.contains_address(javac_address):
-      build_graph.inject_synthetic_target(javac_address, ToolsJar)
+    tools_jar_address = Address.parse(self._tools_jar_spec)
+    if not build_graph.contains_address(tools_jar_address):
+      build_graph.inject_synthetic_target(tools_jar_address, ToolsJar)
+    elif not build_graph.get_target(tools_jar_address).is_synthetic:
+      raise build_graph.ManualSyntheticTargetError(tools_jar_address)
 
   @property
   def injectables_spec_mapping(self):
     return {
       'plugin': self._plugin_dependency_specs,
+      # Zinc directly accesses the javac tool.
       'javac': [self._javac_spec],
+      # The ProvideToolsJar task will first attempt to use the (optional) configured
+      # javac tool, and then fall back to injecting a classpath entry linking to the current
+      # distribution's `tools.jar`.
+      'tools.jar': [self._tools_jar_spec],
     }
 
   @classmethod
@@ -77,6 +82,7 @@ def __init__(self, *args, **kwargs):
     self._plugin_dependency_specs = [
       Address.parse(spec).spec for spec in getattr(opts, 'compiler_plugin_deps', [])
     ]
+    self._tools_jar_spec = '//:tools-jar-synthetic'
 
   def javac_classpath(self, products):
     return self.tool_classpath_from_products(products, 'javac', self.options_scope)
diff --git a/src/python/pants/backend/jvm/targets/javac_plugin.py b/src/python/pants/backend/jvm/targets/javac_plugin.py
--- a/src/python/pants/backend/jvm/targets/javac_plugin.py
+++ b/src/python/pants/backend/jvm/targets/javac_plugin.py
@@ -34,4 +34,4 @@ def compute_dependency_specs(cls, kwargs=None, payload=None):
     for spec in super(JavacPlugin, cls).compute_dependency_specs(kwargs, payload):
       yield spec
 
-    yield Java.global_instance().injectables_spec_for_key('javac')
+    yield Java.global_instance().injectables_spec_for_key('tools.jar')
diff --git a/src/python/pants/backend/jvm/tasks/provide_tools_jar.py b/src/python/pants/backend/jvm/tasks/provide_tools_jar.py
--- a/src/python/pants/backend/jvm/tasks/provide_tools_jar.py
+++ b/src/python/pants/backend/jvm/tasks/provide_tools_jar.py
@@ -7,6 +7,7 @@
 
 import os
 
+from pants.backend.jvm.subsystems.java import Java
 from pants.backend.jvm.targets.tools_jar import ToolsJar
 from pants.backend.jvm.tasks.classpath_products import ClasspathProducts
 from pants.backend.jvm.tasks.jvm_tool_task_mixin import JvmToolTaskMixin
@@ -26,6 +27,14 @@ class ProvideToolsJar(JvmToolTaskMixin):
   def product_types(cls):
     return ['compile_classpath']
 
+  @classmethod
+  def subsystem_dependencies(cls):
+    return super(ProvideToolsJar, cls).subsystem_dependencies() + (Java,)
+
+  @classmethod
+  def implementation_version(cls):
+    return super(ProvideToolsJar, cls).implementation_version() + [('ProvideToolsJar', 2)]
+
   @property
   def create_target_dirs(self):
     # Create-but-don't-cache the target directories. The created symlinks are not portable.
@@ -37,22 +46,36 @@ def execute(self):
 
     with self.invalidated(self.context.targets(is_tools_jar)) as invalidation_check:
       for vt in invalidation_check.all_vts:
-        jar_path = self._jar_path(vt.results_dir)
+        tools_classpath = self._tools_classpath_pairs(vt.results_dir)
         if not vt.valid:
-          self._symlink_tools_jar(jar_path)
-        compile_classpath.add_for_target(vt.target, [('default', jar_path)])
+          self._symlink_tools_classpath(tools_classpath)
+        compile_classpath.add_for_target(vt.target,
+                                         [('default', entry) for _, entry in tools_classpath])
+
+  def _tools_classpath_pairs(self, dest_dir):
+    """Given a destination directory, returns a list of tuples of (src, dst) symlink pairs."""
+    tools_classpath = self._tools_classpath
+    return [(entry, os.path.join(dest_dir, '{}-{}'.format(idx, os.path.basename(entry))))
+            for idx, entry in enumerate(tools_classpath)]
 
   @memoized_property
-  def _tools_jar(self):
+  def _tools_classpath(self):
+    """Returns a classpath representing the (equivalent of the) `tools.jar`.
+
+    If `javac` has been set explicitly, it is used. Otherwise, searches the current distribution.
+    """
+
+    javac_classpath = Java.global_javac_classpath(self.context.products)
+    if javac_classpath:
+      return javac_classpath
+
     self.set_distribution(jdk=True)
     jars = self.dist.find_libs(['tools.jar'])
     if len(jars) != 1:
       raise TaskError('Expected a single `tools.jar` entry for {}; got: {}'.format(
         self.dist, jars))
-    return jars[0]
-
-  def _jar_path(self, chroot):
-    return os.path.join(chroot, 'tools.jar')
+    return jars
 
-  def _symlink_tools_jar(self, dest_jar_path):
-    relative_symlink(self._tools_jar, dest_jar_path)
+  def _symlink_tools_classpath(self, tools_classpath):
+    for src, dst in tools_classpath:
+      relative_symlink(src, dst)


Test patch:
diff --git a/tests/python/pants_test/backend/jvm/tasks/jvm_compile/java/test_zinc_compile_integration.py b/tests/python/pants_test/backend/jvm/tasks/jvm_compile/java/test_zinc_compile_integration.py
--- a/tests/python/pants_test/backend/jvm/tasks/jvm_compile/java/test_zinc_compile_integration.py
+++ b/tests/python/pants_test/backend/jvm/tasks/jvm_compile/java/test_zinc_compile_integration.py
@@ -6,7 +6,7 @@
                         unicode_literals, with_statement)
 
 import os
-from unittest import skip, skipIf
+from unittest import skipIf
 
 from pants.build_graph.address import Address
 from pants.build_graph.target import Target
@@ -199,7 +199,6 @@ def test_no_record_classpath(self):
                               extra_args=['--no-compile-zinc-capture-classpath']) as found:
       self.assertFalse(classpath_filename in found)
 
-  @skip("TODO: Fix post #5016.")
   @skipIf(is_missing_jvm('1.8'), 'no java 1.8 installation on testing machine')
   def test_custom_javac(self):
     with self.temporary_workdir() as workdir: