Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

classmap: a jvm console task that outputs mapping from class products to their targets #4081

Merged
merged 9 commits into from Dec 7, 2016
3 changes: 3 additions & 0 deletions src/python/pants/backend/jvm/register.py
Expand Up @@ -37,6 +37,7 @@
from pants.backend.jvm.tasks.bootstrap_jvm_tools import BootstrapJvmTools
from pants.backend.jvm.tasks.bundle_create import BundleCreate
from pants.backend.jvm.tasks.check_published_deps import CheckPublishedDeps
from pants.backend.jvm.tasks.classmap import ClassmapTask
from pants.backend.jvm.tasks.consolidate_classpath import ConsolidateClasspath
from pants.backend.jvm.tasks.detect_duplicates import DuplicateDetector
from pants.backend.jvm.tasks.ivy_imports import IvyImports
Expand Down Expand Up @@ -166,6 +167,8 @@ def register_goals():

task(name='jvm', action=JvmDependencyUsage).install('dep-usage')

task(name='classmap', action=ClassmapTask).install('classmap')

# Generate documentation.
task(name='javadoc', action=JavadocGen).install('doc')
task(name='scaladoc', action=ScaladocGen).install('doc')
Expand Down
11 changes: 11 additions & 0 deletions src/python/pants/backend/jvm/tasks/BUILD
Expand Up @@ -11,6 +11,7 @@ target(
':consolidate_classpath',
':check_published_deps',
':checkstyle',
':classmap',
':detect_duplicates',
':ivy_imports',
':ivy_resolve',
Expand Down Expand Up @@ -650,3 +651,13 @@ python_library(
'src/python/pants/task',
]
)

python_library(
name = 'classmap',
sources = ['classmap.py'],
dependencies = [
':classpath_util',
'src/python/pants/backend/jvm/targets:jvm',
'src/python/pants/task',
],
)
47 changes: 47 additions & 0 deletions src/python/pants/backend/jvm/tasks/classmap.py
@@ -0,0 +1,47 @@
# coding=utf-8
# Copyright 2016 Pants project contributors (see CONTRIBUTORS.md).
# Licensed under the Apache License, Version 2.0 (see LICENSE).

from __future__ import (absolute_import, division, generators, nested_scopes, print_function,
unicode_literals, with_statement)

from pants.backend.jvm.targets.jar_library import JarLibrary
from pants.backend.jvm.tasks.classpath_util import ClasspathUtil
from pants.task.console_task import ConsoleTask


class ClassmapTask(ConsoleTask):
"""Print a mapping from class name to the owning target from target's runtime classpath."""

@classmethod
def register_options(cls, register):
super(ClassmapTask, cls).register_options(register)

register('--internal-only', default=False, type=bool, fingerprint=True,
help='Specifies that only class names of internal dependencies should be included.')
register('--transitive', default=True, type=bool,
help='Outputs all targets in the build graph transitively.')

def classname_for_classfile(self, target, classpath_products):
contents = ClasspathUtil.classpath_contents((target,), classpath_products)
for f in contents:
classname = ClasspathUtil.classname_for_rel_classfile(f)
# None for non `.class` files
if classname:
yield classname

def console_output(self, _):
def should_ignore(target):
return self.get_options().internal_only and isinstance(target, JarLibrary)
Copy link
Sponsor Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about JvmBinary targets that have a source=?

Might be better to check for the opposite (not internal_only and isinstance(target, JarLibrary).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure I get... see no difference how jvm_binary is treated:

An example jvm_binary (this example has source but if w/o source if would be one less entry in the output of its own class mapping.)

tw-mbp-peiyu:pants peiyu$ ./pants classmap examples/src/java/org/pantsbuild/example/annotation/main --classmap-internal-only
org.pantsbuild.example.annotation.main.Main examples/src/java/org/pantsbuild/example/annotation/main:main
org.pantsbuild.example.annotation.example.Example examples/src/java/org/pantsbuild/example/annotation/example:example
org.pantsbuild.example.annotation.processor.ExampleProcessor examples/src/java/org/pantsbuild/example/annotation/processor:processor

tw-mbp-peiyu:pants peiyu$ ./pants classmap examples/src/java/org/pantsbuild/example/annotation/main --classmap-internal-only --no-classmap-transitive
org.pantsbuild.example.annotation.main.Main examples/src/java/org/pantsbuild/example/annotation/main:main

Copy link
Sponsor Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I think I misread "JavaLibrary" for "JarLibrary". This is fine as-is.


classpath_product = self.context.products.get_data('runtime_classpath')
targets = self.context.targets() if self.get_options().transitive else self.context.target_roots
for target in targets:
if not should_ignore(target):
for file in self.classname_for_classfile(target, classpath_product):
yield '{} {}'.format(file, target.address.spec)

@classmethod
def prepare(cls, options, round_manager):
super(ClassmapTask, cls).prepare(options, round_manager)
round_manager.require_data('runtime_classpath')
5 changes: 0 additions & 5 deletions src/python/pants/backend/jvm/tasks/jvm_compile/jvm_compile.py
Expand Up @@ -576,11 +576,6 @@ def compute_classes_by_source(self, compile_contexts):
classes_by_src[None] = list(unclaimed_classes)
return classes_by_src_by_context

def classname_for_classfile(self, compile_context, class_file_name):
assert class_file_name.startswith(compile_context.classes_dir)
rel_classfile_path = class_file_name[len(compile_context.classes_dir) + 1:]
return ClasspathUtil.classname_for_rel_classfile(rel_classfile_path)

def _register_vts(self, compile_contexts):
Copy link
Sponsor Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this not needed anywhere?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yea, this is not used anywhere

[tw-mbp-peiyu pants (master)]$ git grep classname_for_classfile
src/python/pants/backend/jvm/tasks/jvm_compile/jvm_compile.py: def classname_for_classfile(self, compile_context, class_file_name):

classes_by_source = self.context.products.get_data('classes_by_source')
product_deps_by_src = self.context.products.get_data('product_deps_by_src')
Expand Down
22 changes: 22 additions & 0 deletions tests/python/pants_test/backend/jvm/tasks/BUILD
Expand Up @@ -681,3 +681,25 @@ python_tests(
],
tags = {'integration'},
)

python_tests(
name = 'classmap',
sources = ['test_classmap.py'],
dependencies = [
':jvm_binary_task_test_base',
'src/python/pants/backend/jvm/targets:java',
'src/python/pants/backend/jvm/targets:jvm',
'src/python/pants/backend/jvm/tasks:classmap',
'tests/python/pants_test/jvm:jvm_task_test_base',
'tests/python/pants_test/tasks:task_test_base',
],
)

python_tests(
name = 'classmap_integration',
sources = ['test_classmap_integration.py'],
dependencies = [
'tests/python/pants_test/tasks:task_test_base',
],
tags = {'integration'},
)
79 changes: 79 additions & 0 deletions tests/python/pants_test/backend/jvm/tasks/test_classmap.py
@@ -0,0 +1,79 @@
# coding=utf-8
# Copyright 2016 Pants project contributors (see CONTRIBUTORS.md).
# Licensed under the Apache License, Version 2.0 (see LICENSE).

from __future__ import (absolute_import, division, generators, nested_scopes, print_function,
unicode_literals, with_statement)

from contextlib import contextmanager

from pants.backend.jvm.targets.jar_dependency import JarDependency
from pants.backend.jvm.targets.jar_library import JarLibrary
from pants.backend.jvm.targets.java_library import JavaLibrary
from pants.backend.jvm.tasks.classmap import ClassmapTask
from pants.build_graph.target import Target
from pants.util.contextutil import open_zip
from pants_test.backend.jvm.tasks.jvm_binary_task_test_base import JvmBinaryTaskTestBase
from pants_test.subsystem.subsystem_util import init_subsystem
from pants_test.tasks.task_test_base import ConsoleTaskTestBase


class ClassmapTaskTest(ConsoleTaskTestBase, JvmBinaryTaskTestBase):
@classmethod
def task_type(cls):
return ClassmapTask

def setUp(self):
super(ClassmapTaskTest, self).setUp()
init_subsystem(Target.Arguments)

self.target_a = self.make_target('a', target_type=JavaLibrary, sources=['a1.java', 'a2.java'])

self.jar_artifact = self.create_artifact(org='org.example', name='foo', rev='1.0.0')
with open_zip(self.jar_artifact.pants_path, 'w') as jar:
jar.writestr('foo/Foo.class', '')
self.target_b = self.make_target('b', target_type=JarLibrary,
jars=[JarDependency(org='org.example', name='foo', rev='1.0.0')])

self.target_c = self.make_target('c', dependencies=[self.target_a, self.target_b],
target_type=JavaLibrary)

@contextmanager
def prepare_context(self, options=None):
def idict(*args):
return {a: a for a in args}

options = options or {}
self.set_options(**options)

task_context = self.context(target_roots=[self.target_c])
self.add_to_runtime_classpath(task_context, self.target_a, idict('a1.class', 'a2.class'))
self.add_to_runtime_classpath(task_context, self.target_c, idict('c1.class', 'c2.class'))

classpath_products = self.ensure_classpath_products(task_context)
classpath_products.add_jars_for_targets(targets=[self.target_b],
conf='default',
resolved_jars=[self.jar_artifact])
yield task_context

def test_classmap_none(self):
class_mappings = self.execute_console_task()
self.assertEqual([], class_mappings)

def test_classmap(self):
with self.prepare_context() as context:
class_mappings = self.execute_console_task_given_context(context)
class_mappings_expected = ['a1 a:a', 'a2 a:a', 'c1 c:c', 'c2 c:c', 'foo.Foo b:b']
self.assertEqual(class_mappings_expected, sorted(class_mappings))

def test_classmap_internal_only(self):
with self.prepare_context(options={'internal_only': True}) as context:
class_mappings = self.execute_console_task_given_context(context)
class_mappings_expected = ['a1 a:a', 'a2 a:a', 'c1 c:c', 'c2 c:c']
self.assertEqual(class_mappings_expected, sorted(class_mappings))

def test_classmap_intransitive(self):
with self.prepare_context(options={'transitive': False}) as context:
class_mappings = self.execute_console_task_given_context(context)
class_mappings_expected = ['c1 c:c', 'c2 c:c']
self.assertEqual(class_mappings_expected, sorted(class_mappings))
@@ -0,0 +1,40 @@
# coding=utf-8
# Copyright 2016 Pants project contributors (see CONTRIBUTORS.md).
# Licensed under the Apache License, Version 2.0 (see LICENSE).

from __future__ import (absolute_import, division, generators, nested_scopes, print_function,
unicode_literals, with_statement)

from pants_test.pants_run_integration_test import PantsRunIntegrationTest


class ClassmapTaskIntegrationTest(PantsRunIntegrationTest):
# A test target with both transitive internal dependency as well as external dependency
TEST_JVM_TARGET = 'testprojects/tests/java/org/pantsbuild/testproject/testjvms:seven'
INTERNAL_MAPPING = ('org.pantsbuild.testproject.testjvms.TestSeven '
'testprojects/tests/java/org/pantsbuild/testproject/testjvms:seven')
INTERNAL_TRANSITIVE_MAPPING = ('org.pantsbuild.testproject.testjvms.TestBase '
'testprojects/tests/java/org/pantsbuild/testproject/testjvms:base')
EXTERNAL_MAPPING = ('org.junit.ClassRule 3rdparty:junit')

def test_classmap_none(self):
pants_run = self.do_command('classmap', success=True)
self.assertEqual(len(pants_run.stdout_data.strip().split()), 0)

def test_classmap(self):
pants_run = self.do_command('classmap', self.TEST_JVM_TARGET, success=True)
self.assertIn(self.INTERNAL_MAPPING, pants_run.stdout_data)
self.assertIn(self.INTERNAL_TRANSITIVE_MAPPING, pants_run.stdout_data)
self.assertIn(self.EXTERNAL_MAPPING, pants_run.stdout_data)

def test_classmap_internal_only(self):
pants_run = self.do_command('classmap', '--internal-only', self.TEST_JVM_TARGET, success=True)
self.assertIn(self.INTERNAL_MAPPING, pants_run.stdout_data)
self.assertIn(self.INTERNAL_TRANSITIVE_MAPPING, pants_run.stdout_data)
self.assertNotIn(self.EXTERNAL_MAPPING, pants_run.stdout_data)

def test_classmap_intransitive(self):
pants_run = self.do_command('classmap', '--no-transitive', self.TEST_JVM_TARGET, success=True)
self.assertIn(self.INTERNAL_MAPPING, pants_run.stdout_data)
self.assertNotIn(self.INTERNAL_TRANSITIVE_MAPPING, pants_run.stdout_data)
self.assertNotIn(self.EXTERNAL_MAPPING, pants_run.stdout_data)