diff --git a/3rdparty/BUILD b/3rdparty/BUILD
index f84504dd476..107553cc58b 100644
--- a/3rdparty/BUILD
+++ b/3rdparty/BUILD
@@ -122,6 +122,11 @@ jar_library(name='easymock',
jar('org.easymock', 'easymock', '3.3.1')
])
+jar_library(name='gson',
+ jars = [
+ jar(org='com.google.code.gson', name='gson', rev='2.3.1')
+ ])
+
jar_library(name='guava-testlib',
jars=[
jar('com.google.guava', 'guava-testlib', '18.0')
diff --git a/examples/src/java/org/pantsbuild/example/readme.md b/examples/src/java/org/pantsbuild/example/readme.md
index b35d7614684..6506643994e 100644
--- a/examples/src/java/org/pantsbuild/example/readme.md
+++ b/examples/src/java/org/pantsbuild/example/readme.md
@@ -416,6 +416,70 @@ After building our `hello` example, if we check the binary jar's contents, there
org/pantsbuild/example/hello/world.txt
$
+Shading
+-------
+
+Sometimes you have dependencies that have conflicting package or class names. This typically occurs
+in the following scenario: Your jvm_binary depends on a 3rdparty library A (rev 1.0), and a 3rdparty
+library B (rev 1.3). It turns out that A happens to also depend on B, but it depends on B (rev 2.0),
+which is backwards-incompatible with rev 1.3. Now B (1.3) and B (2.0) define different versions of
+the same classes, with the same fully-qualified class names, and you're pulling them all onto the
+classpath for your project.
+
+This is where shading comes in: you can rename the fully-qualified names of the classes that
+conflict, typically by applying a prefix (eg, `__shaded_by_pants__.org.foobar.example`).
+
+Pants uses jarjar for shading, and allows shading rules to be specified on `jvm_binary` targets with
+the `shading_rules` argument. The `shading_rules` argument is a list of rules. Available rules
+include: `shading_relocate` ,
+`shading_exclude` ,
+`shading_relocate_package` , and
+`shading_exclude_package` .
+
+The order of rules in the list matters, as typical of shading
+logic in general.
+
+These rules are powerful enough to take advantage of jarjar's more
+advanced syntax, like using wildcards in the middle of package
+names. E.g., this syntax works:
+
+ :::python
+ # Destination pattern will be inferred to be
+ # __shaded_by_pants__.com.@1.foo.bar.@2
+ shading_relocate('com.*.foo.bar.**')
+
+Which can also be done by:
+
+ :::python
+ shading_relocate_package('com.*.foo.bar')
+
+The default shading prefix is `__shaded_by_pants__`, but you can change it:
+
+ :::python
+ shading_relocate_package('com.foo.bar', shade_prefix='__my_prefix__.')
+
+You can rename a specific class:
+
+ :::python
+ shading_relocate('com.example.foo.Main', 'org.example.bar.NotMain')
+
+If you want to shade everything in a package except a particular file (or subpackage), you can use
+the `shading_exclude` rule.
+
+ :::python
+ shading_exclude('com.example.foobar.Main') # Omit the Main class.
+ shading_exclude_package('com.example.foobar.api') # Omit the api subpackage.
+ shading_relocate_package('com.example.foobar')
+
+Again, order matters here: excludes have to appear __first__.
+
+To see an example, take a look at `testprojects/src/java/org/pantsbuild/testproject/shading/BUILD`,
+and try running
+
+ :::bash
+ ./pants binary testprojects/src/java/org/pantsbuild/testproject/shading
+ jar -tf dist/shading.jar
+
Further Reading
---------------
diff --git a/src/python/pants/backend/jvm/BUILD b/src/python/pants/backend/jvm/BUILD
index 273f44b2209..16e0de570f1 100644
--- a/src/python/pants/backend/jvm/BUILD
+++ b/src/python/pants/backend/jvm/BUILD
@@ -30,6 +30,7 @@ python_library(
'src/python/pants/base:build_file_aliases',
'src/python/pants/goal',
'src/python/pants/goal:task_registrar',
+ 'src/python/pants/java/jar:shader',
],
)
@@ -64,4 +65,4 @@ python_library(
':artifact',
'src/python/pants/base:validation',
],
-)
\ No newline at end of file
+)
diff --git a/src/python/pants/backend/jvm/register.py b/src/python/pants/backend/jvm/register.py
index 8b15831ff55..6919d7c8d48 100644
--- a/src/python/pants/backend/jvm/register.py
+++ b/src/python/pants/backend/jvm/register.py
@@ -52,6 +52,7 @@
from pants.base.build_file_aliases import BuildFileAliases
from pants.goal.goal import Goal
from pants.goal.task_registrar import TaskRegistrar as task
+from pants.java.jar.shader import Shading
def build_file_aliases():
@@ -86,6 +87,10 @@ def build_file_aliases():
'jar_rules': JarRules,
'Repository': Repository,
'Skip': Skip,
+ 'shading_relocate': Shading.Relocate.new,
+ 'shading_exclude': Shading.Exclude.new,
+ 'shading_relocate_package': Shading.RelocatePackage.new,
+ 'shading_exclude_package': Shading.ExcludePackage.new,
},
context_aware_object_factories={
'bundle': Bundle.factory,
diff --git a/src/python/pants/backend/jvm/targets/jvm_binary.py b/src/python/pants/backend/jvm/targets/jvm_binary.py
index 30078275cc3..99ed7ef80b5 100644
--- a/src/python/pants/backend/jvm/targets/jvm_binary.py
+++ b/src/python/pants/backend/jvm/targets/jvm_binary.py
@@ -291,6 +291,7 @@ def __init__(self,
deploy_excludes=None,
deploy_jar_rules=None,
manifest_entries=None,
+ shading_rules=None,
**kwargs):
"""
:param string main: The name of the ``main`` class, e.g.,
@@ -300,9 +301,6 @@ def __init__(self,
``'hello'``. (By default, uses ``name`` param)
:param string source: Name of one ``.java`` or ``.scala`` file (a good
place for a ``main``).
- :param sources: Overridden by source. If you want more than one source
- file, use a library and have the jvm_binary depend on that library.
- :param resources: List of ``resource``\s to include in bundle.
:param dependencies: Targets (probably ``java_library`` and
``scala_library`` targets) to "link" in.
:type dependencies: list of target specs
@@ -316,9 +314,10 @@ def __init__(self,
deploy jar.
:param manifest_entries: dict that specifies entries for `ManifestEntries <#manifest_entries>`_
for adding to MANIFEST.MF when packaging this binary.
- :param configurations: Ivy configurations to resolve for this target.
- This parameter is not intended for general use.
- :type configurations: tuple of strings
+ :param list shading_rules: Optional list of shading rules to apply when building a shaded
+ (aka monolithic aka fat) binary jar. The order of the rules matters: the first rule which
+ matches a fully-qualified class name is used to shade it. See shading_relocate(),
+ shading_exclude(), shading_relocate_package(), and shading_exclude_package().
"""
self.address = address # Set in case a TargetDefinitionException is thrown early
if main and not isinstance(main, string_types):
@@ -349,7 +348,8 @@ def __init__(self,
'deploy_jar_rules': FingerprintedField(deploy_jar_rules or JarRules.default()),
'manifest_entries': FingerprintedField(ManifestEntries(manifest_entries)),
'main': PrimitiveField(main),
- })
+ 'shading_rules': PrimitiveField(shading_rules or ()),
+ })
super(JvmBinary, self).__init__(name=name,
address=address,
@@ -369,6 +369,10 @@ def deploy_excludes(self):
def deploy_jar_rules(self):
return self.payload.deploy_jar_rules
+ @property
+ def shading_rules(self):
+ return self.payload.shading_rules
+
@property
def main(self):
return self.payload.main
diff --git a/src/python/pants/backend/jvm/tasks/BUILD b/src/python/pants/backend/jvm/tasks/BUILD
index 75d2741007e..0d8561c4533 100644
--- a/src/python/pants/backend/jvm/tasks/BUILD
+++ b/src/python/pants/backend/jvm/tasks/BUILD
@@ -70,12 +70,12 @@ python_library(
'src/python/pants/base:exceptions',
'src/python/pants/base:workunit',
'src/python/pants/ivy',
- 'src/python/pants/java/distribution',
'src/python/pants/java:executor',
'src/python/pants/java:util',
'src/python/pants/java/jar:shader',
'src/python/pants/backend/jvm/subsystems:jvm_tool_mixin',
'src/python/pants/util:dirutil',
+ 'src/python/pants/util:memo',
],
)
@@ -290,6 +290,11 @@ python_library(
':jar_task',
'3rdparty/python/twitter/commons:twitter.common.collections',
'src/python/pants/backend/jvm/targets:jvm',
+ 'src/python/pants/base:exceptions',
+ 'src/python/pants/java/jar:shader',
+ 'src/python/pants/util:contextutil',
+ 'src/python/pants/util:fileutil',
+ 'src/python/pants/util:memo',
],
)
diff --git a/src/python/pants/backend/jvm/tasks/bootstrap_jvm_tools.py b/src/python/pants/backend/jvm/tasks/bootstrap_jvm_tools.py
index e3cdd664f33..ffccce67186 100644
--- a/src/python/pants/backend/jvm/tasks/bootstrap_jvm_tools.py
+++ b/src/python/pants/backend/jvm/tasks/bootstrap_jvm_tools.py
@@ -20,10 +20,10 @@
from pants.base.exceptions import TaskError
from pants.ivy.ivy_subsystem import IvySubsystem
from pants.java import util
-from pants.java.distribution.distribution import DistributionLocator
-from pants.java.executor import Executor, SubprocessExecutor
+from pants.java.executor import Executor
from pants.java.jar.shader import Shader
from pants.util.dirutil import safe_mkdir_for
+from pants.util.memo import memoized_property
class ShadedToolFingerprintStrategy(IvyResolveFingerprintStrategy):
@@ -75,11 +75,10 @@ def register_options(cls, register):
super(BootstrapJvmTools, cls).register_options(register)
register('--jvm-options', action='append', metavar='...',
help='Run the tool shader with these extra jvm options.')
- cls.register_jvm_tool(register, 'jarjar')
@classmethod
def subsystem_dependencies(cls):
- return super(BootstrapJvmTools, cls).subsystem_dependencies() + (DistributionLocator,)
+ return super(BootstrapJvmTools, cls).subsystem_dependencies() + (Shader.Factory,)
@classmethod
def global_subsystems(cls):
@@ -142,12 +141,9 @@ def _bootstrap_tool_classpath(self, key, scope, tools):
targets = list(self._resolve_tool_targets(tools, key, scope))
return self._bootstrap_classpath(key, targets)
- @property
+ @memoized_property
def shader(self):
- if self._shader is None:
- jarjar = self.tool_jar('jarjar')
- self._shader = Shader(jarjar, SubprocessExecutor(DistributionLocator.cached()))
- return self._shader
+ return Shader.Factory.create(self.context)
def _bootstrap_shaded_jvm_tool(self, key, scope, tools, main, custom_rules=None):
targets = list(self._resolve_tool_targets(tools, key, scope))
diff --git a/src/python/pants/backend/jvm/tasks/bundle_create.py b/src/python/pants/backend/jvm/tasks/bundle_create.py
index ee4ba67a3b6..679d9253d97 100644
--- a/src/python/pants/backend/jvm/tasks/bundle_create.py
+++ b/src/python/pants/backend/jvm/tasks/bundle_create.py
@@ -66,9 +66,9 @@ def execute(self):
for target in self.context.target_roots:
for app in map(self.App, filter(self.App.is_app, [target])):
basedir = self.bundle(app)
- # NB(Eric Ayers): Note that this product is not housed/controlled under .pants.d/ Since
+ # NB(Eric Ayers): Note that this product is not housed/controlled under .pants.d/ Since
# the bundle is re-created every time, this shouldn't cause a problem, but if we ever
- # expect the product to be cached, a user running an 'rm' on the dist/ directory could
+ # expect the product to be cached, a user running an 'rm' on the dist/ directory could
# cause inconsistencies.
jvm_bundles_product = self.context.products.get('jvm_bundles')
jvm_bundles_product.add(target, os.path.dirname(basedir)).append(os.path.basename(basedir))
@@ -123,7 +123,11 @@ def add_jars(target):
# Add external dependencies to the bundle.
for basedir, external_jar in self.list_external_jar_dependencies(app.binary):
path = os.path.join(basedir, external_jar)
- verbose_symlink(path, os.path.join(lib_dir, external_jar))
+ destination = os.path.join(lib_dir, external_jar)
+ verbose_symlink(path, destination)
+ if app.binary.shading_rules:
+ self.shade_jar(binary=app.binary, jar_id=os.path.basename(external_jar),
+ jar_path=destination)
classpath.add(external_jar)
bundle_jar = os.path.join(bundle_dir, '{}.jar'.format(app.binary.basename))
diff --git a/src/python/pants/backend/jvm/tasks/jvm_binary_task.py b/src/python/pants/backend/jvm/tasks/jvm_binary_task.py
index 518507aac1a..10c699d9efc 100644
--- a/src/python/pants/backend/jvm/tasks/jvm_binary_task.py
+++ b/src/python/pants/backend/jvm/tasks/jvm_binary_task.py
@@ -13,6 +13,12 @@
from pants.backend.jvm.targets.jvm_binary import JvmBinary
from pants.backend.jvm.tasks.jar_task import JarTask
+from pants.base.exceptions import TaskError
+from pants.java.jar.shader import Shader
+from pants.java.util import execute_runner
+from pants.util.contextutil import temporary_dir
+from pants.util.fileutil import atomic_copy
+from pants.util.memo import memoized_property
class JvmBinaryTask(JarTask):
@@ -37,6 +43,10 @@ def prepare(cls, options, round_manager):
round_manager.require('jar_dependencies', predicate=cls.is_binary)
cls.JarBuilder.prepare(round_manager)
+ @classmethod
+ def subsystem_dependencies(cls):
+ return super(JvmBinaryTask, cls).subsystem_dependencies() + (Shader.Factory,)
+
def list_external_jar_dependencies(self, binary, confs=None):
"""Returns the external jar dependencies of the given binary.
@@ -61,25 +71,60 @@ def monolithic_jar(self, binary, path, with_external_deps):
"""
# TODO(benjy): There's actually nothing here that requires 'binary' to be a jvm_binary.
# It could be any target. And that might actually be useful.
-
with self.context.new_workunit(name='create-monolithic-jar'):
with self.open_jar(path,
jar_rules=binary.deploy_jar_rules,
overwrite=True,
- compressed=True) as jar:
+ compressed=True) as monolithic_jar:
with self.context.new_workunit(name='add-internal-classes'):
- with self.create_jar_builder(jar) as jar_builder:
+ with self.create_jar_builder(monolithic_jar) as jar_builder:
jar_builder.add_target(binary, recursive=True)
if with_external_deps:
+ # NB(gmalmquist): Shading each jar dependency with its own prefix would be a nice feature,
+ # but is not currently possible with how things are set up. It may not be possible to do
+ # in general, at least efficiently.
with self.context.new_workunit(name='add-dependency-jars'):
for basedir, external_jar in self.list_external_jar_dependencies(binary):
- external_jar_path = os.path.join(basedir, external_jar)
- self.context.log.debug(' dumping {}'.format(external_jar_path))
- jar.writejar(external_jar_path)
+ jar_path = os.path.join(basedir, external_jar)
+ self.context.log.debug(' dumping {}'.format(jar_path))
+ monolithic_jar.writejar(jar_path)
+
+ yield monolithic_jar
+
+ if binary.shading_rules:
+ with self.context.new_workunit('shade-monolithic-jar'):
+ self.shade_jar(binary=binary, jar_id=binary.address.reference(), jar_path=path)
- yield jar
+ @memoized_property
+ def shader(self):
+ return Shader.Factory.create(self.context)
+
+ def shade_jar(self, binary, jar_id, jar_path):
+ """Shades a jar using the shading rules from the given jvm_binary.
+
+ This *overwrites* the existing jar file at ``jar_path``.
+
+ :param binary: The jvm_binary target the jar is being shaded for.
+ :param jar_id: The id of the jar being shaded (used for logging).
+ :param jar_path: The filepath to the jar that should be shaded.
+ """
+ self.context.log.debug('Shading {} at {}.'.format(jar_id, jar_path))
+ with temporary_dir() as tempdir:
+ output_jar = os.path.join(tempdir, os.path.basename(jar_path))
+ rules = [rule.rule() for rule in binary.shading_rules]
+ with self.shader.binary_shader_for_rules(output_jar, jar_path, rules) as shade_runner:
+ result = execute_runner(shade_runner, workunit_factory=self.context.new_workunit,
+ workunit_name='jarjar')
+ if result != 0:
+ raise TaskError('Shading tool failed to shade {0} (error code {1})'.format(jar_path,
+ result))
+ if not os.path.exists(output_jar):
+ raise TaskError('Shading tool returned success for {0}, but '
+ 'the output jar was not found at {1}'.format(jar_path, output_jar))
+ atomic_copy(output_jar, jar_path)
+ return jar_path
def _mapped_dependencies(self, jardepmap, binary, confs):
# TODO(John Sirois): rework product mapping towards well known types
diff --git a/src/python/pants/java/jar/BUILD b/src/python/pants/java/jar/BUILD
index c25b23b90c2..4a079ee66ae 100644
--- a/src/python/pants/java/jar/BUILD
+++ b/src/python/pants/java/jar/BUILD
@@ -13,6 +13,8 @@ python_library(
name='shader',
sources=['shader.py'],
dependencies=[
- 'src/python/pants/util:contextutil'
+ 'src/python/pants/util:contextutil',
+ 'src/python/pants/java/distribution',
+ 'src/python/pants/subsystem',
]
)
diff --git a/src/python/pants/java/jar/shader.py b/src/python/pants/java/jar/shader.py
index 51c03faca91..a8e20ad6104 100644
--- a/src/python/pants/java/jar/shader.py
+++ b/src/python/pants/java/jar/shader.py
@@ -6,20 +6,19 @@
unicode_literals, with_statement)
import os
+import re
from collections import namedtuple
from contextlib import contextmanager
+from pants.backend.jvm.tasks.jvm_tool_task_mixin import JvmToolMixin
+from pants.java.distribution.distribution import DistributionLocator
+from pants.java.executor import SubprocessExecutor
+from pants.subsystem.subsystem import Subsystem, SubsystemError
from pants.util.contextutil import open_zip, temporary_file
-# TODO(John Sirois): Support shading given an input jar and a set of user-supplied rules (these
-# will come from target attributes) instead of only supporting auto-generating rules from the main
-# class of the input jar.
-class Shader(object):
- """Creates shaded jars."""
-
- class Error(Exception):
- """Indicates an error shading a jar."""
+class Shading(object):
+ """Wrapper around relocate and exclude shading rules exposed in BUILD files."""
class Rule(namedtuple('Rule', ['from_pattern', 'to_pattern'])):
"""Represents a transformation rule for a jar shading session."""
@@ -28,27 +27,181 @@ def render(self):
return 'rule {0} {1}\n'.format(self.from_pattern, self.to_pattern)
SHADE_PREFIX = '__shaded_by_pants__.'
- """The shading package."""
+ """The default shading package."""
+
+ class Relocate(namedtuple('ShadingRule', ['from_pattern', 'shade_pattern', 'shade_prefix'])):
+ """Base class for shading relocation rules specifyable in BUILD files."""
+
+ _wildcard_pattern = re.compile('[*]+')
+ _starts_with_number_pattern = re.compile('^[0-9]')
+ _illegal_package_char_pattern = re.compile('[^a-z0-9_]', re.I)
+
+ @classmethod
+ def _infer_shaded_pattern_iter(cls, from_pattern, prefix=None):
+ if prefix:
+ yield prefix
+ last = 0
+ for i, match in enumerate(cls._wildcard_pattern.finditer(from_pattern)):
+ yield from_pattern[last:match.start()]
+ yield '@{}'.format(i+1)
+ last = match.end()
+ yield from_pattern[last:]
+
+ @classmethod
+ def _sanitize_package_name(cls, name):
+ parts = name.split('.')
+ for i, part in enumerate(parts):
+ part = cls._illegal_package_char_pattern.sub('_', part)
+ if cls._starts_with_number_pattern.match(part):
+ part = '_{}'.format(part)
+ parts[i] = part
+ return '.'.join(filter(None, parts))
+
+ @classmethod
+ def new(cls, from_pattern, shade_pattern=None, shade_prefix=None):
+ """Creates a rule which shades jar entries from one pattern to another.
+
+ Examples: ::
+
+ # Rename everything in the org.foobar.example package
+ # to __shaded_by_pants__.org.foobar.example.
+ shading_relocate('org.foobar.example.**')
+
+ # Rename org.foobar.example.Main to __shaded_by_pants__.org.foobar.example.Main
+ shading_relocate('org.foobar.example.Main')
+
+ # Rename org.foobar.example.Main to org.foobar.example.NotMain
+ shading_relocate('org.foobar.example.Main', 'org.foobar.example.NotMain')
+
+ # Rename all 'Main' classes under any direct subpackage of org.foobar.
+ shading_relocate('org.foobar.*.Main')
+
+ # Rename org.foobar package to com.barfoo package
+ shading_relocate('org.foobar.**', 'com.barfoo.@1')
+
+ # Rename everything in org.foobar.example package to __hello__.org.foobar.example
+ shading_relocate('org.foobar.example.**', shade_prefix='__hello__')
+
+ :param string from_pattern: Any fully-qualified classname which matches this pattern will be
+ shaded. '*' is a wildcard that matches any individual package component, and '**' is a
+ wildcard that matches any trailing pattern (ie the rest of the string).
+ :param string shade_pattern: The shaded pattern to use, where ``@1``, ``@2``, ``@3``, etc are
+ references to the groups matched by wildcards (groups are numbered from left to right). If
+ omitted, this pattern is inferred from the input pattern, prefixed by the ``shade_prefix``
+ (if provided). (Eg, a ``from_pattern`` of ``com.*.foo.bar.**`` implies a default
+ ``shade_pattern`` of ``__shaded_by_pants__.com.@1.foo.@2``)
+ :param string shade_prefix: Prefix to prepend when generating a ``shade_pattern`` (if a
+ ``shade_pattern`` is not provided by the user). Defaults to '``__shaded_by_pants__.``'.
+ """
+ if shade_prefix is None:
+ # NB(gmalmquist): Have have to check "is None" rather than just one-lining this with an or
+ # statement, because the empty-string is a valid prefix which should not be replaced by the
+ # default prefix.
+ shade_prefix = Shading.SHADE_PREFIX
+ return cls(from_pattern, shade_pattern, shade_prefix)
+
+ def rule(self):
+ from_pattern = self.from_pattern
+ shade_prefix = self.shade_prefix
+ shade_pattern = self.shade_pattern or ''.join(self._infer_shaded_pattern_iter(from_pattern,
+ shade_prefix))
+ return Shading.Rule(from_pattern, shade_pattern)
+
+ class Exclude(Relocate):
+ """Creates a rule which excludes the given pattern from shading."""
+
+ @classmethod
+ def new(cls, pattern):
+ """Creates a rule which excludes the given pattern from shading.
+
+ Examples: ::
+
+ # Don't shade the org.foobar.example.Main class
+ shading_exclude('org.foobar.example.Main')
+
+ # Don't shade anything under org.foobar.example
+ shading_exclude('org.foobar.example.**')
+
+ :param string pattern: Any fully-qualified classname which matches this pattern will NOT be
+ shaded. '*' is a wildcard that matches any individual package component, and '**' is a
+ wildcard that matches any trailing pattern (ie the rest of the string).
+ """
+ return super(Shading.Exclude, cls).new(pattern, shade_prefix='')
+
+ class RelocatePackage(Relocate):
+ """Convenience constructor for a package relocation rule."""
+
+ @classmethod
+ def new(self, package_name, shade_prefix=None, recursive=True):
+ """Convenience constructor for a package relocation rule.
+
+ Essentially equivalent to just using ``shading_relocate('package_name.**')``.
+
+ :param string package_name: Package name to shade (eg, ``org.pantsbuild.example``).
+ :param string shade_prefix: Optional prefix to apply to the package. Defaults to
+ ``__shaded_by_pants__.``.
+ :param bool recursive: Whether to rename everything under any subpackage of ``package_name``,
+ or just direct children of the package. (Defaults to True).
+ """
+ from_pattern = '{package}.{capture}'.format(package=package_name,
+ capture='**' if recursive else '*')
+ return super(Shading.RelocatePackage, self).new(from_pattern=from_pattern,
+ shade_prefix=shade_prefix)
+
+ class ExcludePackage(Relocate):
+ """Convenience constructor for a package exclusion rule."""
+
+ @classmethod
+ def new(cls, package_name, recursive=True):
+ """Convenience constructor for a package exclusion rule.
+
+ Essentially equivalent to just using ``shading_exclude('package_name.**')``.
+
+ :param string package_name: Package name to exclude (eg, ``org.pantsbuild.example``).
+ :param bool recursive: Whether to exclude everything under any subpackage of ``package_name``,
+ or just direct children of the package. (Defaults to True).
+ """
+ from_pattern = '{package}.{capture}'.format(package=package_name,
+ capture='**' if recursive else '*')
+ return super(Shading.ExcludePackage, cls).new(from_pattern=from_pattern, shade_prefix='')
- @classmethod
- def _package_rule(cls, package_name=None, recursive=False, shade=False):
- args = dict(package=package_name,
- capture='**' if recursive else '*',
- dest_prefix=cls.SHADE_PREFIX if shade else '')
-
- if package_name:
- return cls.Rule(from_pattern='{package}.{capture}'.format(**args),
- to_pattern='{dest_prefix}{package}.@1'.format(**args))
- else:
- return cls.Rule(from_pattern='{capture}'.format(**args),
- to_pattern='{dest_prefix}@1'.format(**args))
- @classmethod
- def _class_rule(cls, class_name, shade=False):
- args = dict(class_name=class_name,
- dest_prefix=cls.SHADE_PREFIX if shade else '')
+class Shader(object):
+ """Creates shaded jars."""
- return cls.Rule(from_pattern=class_name, to_pattern='{dest_prefix}{class_name}'.format(**args))
+ class Error(Exception):
+ """Indicates an error shading a jar."""
+
+ class Factory(JvmToolMixin, Subsystem):
+ options_scope = 'shader'
+
+ class Error(SubsystemError):
+ """Error creating a Shader with the Shader.Factory subsystem."""
+
+ @classmethod
+ def subsystem_dependencies(cls):
+ return super(Shader.Factory, cls).subsystem_dependencies() + (DistributionLocator,)
+
+ @classmethod
+ def register_options(cls, register):
+ super(Shader.Factory, cls).register_options(register)
+ cls.register_jvm_tool(register, 'jarjar')
+
+ @classmethod
+ def create(cls, context, executor=None):
+ """Creates and returns a new Shader.
+
+ :param Executor executor: Optional java executor to run jarjar with.
+ """
+ if executor is None:
+ executor = SubprocessExecutor(DistributionLocator.cached())
+ classpath = cls.global_instance().tool_classpath_from_products(context.products, 'jarjar',
+ cls.options_scope)
+ if len(classpath) != 1:
+ raise cls.Error('JarJar classpath is expected to have exactly one jar!\nclasspath = {}'
+ .format(':'.join(classpath)))
+ jarjar = classpath[0]
+ return Shader(jarjar, executor)
@classmethod
def exclude_package(cls, package_name=None, recursive=False):
@@ -60,7 +213,9 @@ def exclude_package(cls, package_name=None, recursive=False):
`False` by default.
:returns: A `Shader.Rule` describing the shading exclusion.
"""
- return cls._package_rule(package_name, recursive, shade=False)
+ if not package_name:
+ return Shading.Exclude.new('**' if recursive else '*').rule()
+ return Shading.ExcludePackage.new(package_name, recursive=recursive).rule()
@classmethod
def exclude_class(cls, class_name):
@@ -69,7 +224,7 @@ def exclude_class(cls, class_name):
:param unicode class_name: A fully qualified classname, eg: `org.pantsbuild.tools.jar.Main`.
:returns: A `Shader.Rule` describing the shading exclusion.
"""
- return cls._class_rule(class_name, shade=False)
+ return Shading.Exclude.new(class_name).rule()
@classmethod
def shade_package(cls, package_name=None, recursive=False):
@@ -81,7 +236,9 @@ def shade_package(cls, package_name=None, recursive=False):
`False` by default.
:returns: A `Shader.Rule` describing the packages to be shaded.
"""
- return cls._package_rule(package_name, recursive, shade=True)
+ if not package_name:
+ return Shading.Relocate.new('**' if recursive else '*').rule()
+ return Shading.RelocatePackage.new(package_name, recursive=recursive).rule()
@classmethod
def shade_class(cls, class_name):
@@ -90,7 +247,7 @@ def shade_class(cls, class_name):
:param unicode class_name: A fully qualified classname, eg: `org.pantsbuild.tools.jar.Main`.
:returns: A `Shader.Rule` describing the class shading.
"""
- return cls._class_rule(class_name, shade=True)
+ return Shading.Relocate.new(class_name).rule()
@staticmethod
def _iter_packages(paths):
@@ -205,6 +362,29 @@ def assemble_binary_rules(self, main, jar, custom_rules=None):
return rules
@contextmanager
+ def binary_shader_for_rules(self, output_jar, jar, rules, jvm_options=None):
+ """Yields an `Executor.Runner` that will perform shading of the binary `jar` when `run()`.
+
+ No default rules are applied; only the rules passed in as a parameter will be used.
+
+ :param unicode output_jar: The path to dump the shaded jar to; will be over-written if it
+ exists.
+ :param unicode jar: The path to the jar file to shade.
+ :param list rules: The rules to apply for shading.
+ :param list jvm_options: an optional sequence of options for the underlying jvm
+ :returns: An `Executor.Runner` that can be `run()` to shade the given `jar`.
+ :rtype: :class:`pants.java.executor.Executor.Runner`
+ """
+ with temporary_file() as fp:
+ for rule in rules:
+ fp.write(rule.render())
+ fp.close()
+
+ yield self._executor.runner(classpath=[self._jarjar],
+ main='org.pantsbuild.jarjar.Main',
+ jvm_options=jvm_options,
+ args=['process', fp.name, jar, output_jar])
+
def binary_shader(self, output_jar, main, jar, custom_rules=None, jvm_options=None):
"""Yields an `Executor.Runner` that will perform shading of the binary `jar` when `run()`.
@@ -224,13 +404,7 @@ def binary_shader(self, output_jar, main, jar, custom_rules=None, jvm_options=No
:param list custom_rules: An optional list of custom `Shader.Rule`s.
:param list jvm_options: an optional sequence of options for the underlying jvm
:returns: An `Executor.Runner` that can be `run()` to shade the given `jar`.
+ :rtype: :class:`pants.java.executor.Executor.Runner`
"""
- with temporary_file() as fp:
- for rule in self.assemble_binary_rules(main, jar, custom_rules=custom_rules):
- fp.write(rule.render())
- fp.close()
-
- yield self._executor.runner(classpath=[self._jarjar],
- main='org.pantsbuild.jarjar.Main',
- jvm_options=jvm_options,
- args=['process', fp.name, jar, output_jar])
+ all_rules = self.assemble_binary_rules(main, jar, custom_rules=custom_rules)
+ return self.binary_shader_for_rules(output_jar, jar, all_rules, jvm_options=jvm_options)
diff --git a/testprojects/src/java/org/pantsbuild/testproject/shading/BUILD b/testprojects/src/java/org/pantsbuild/testproject/shading/BUILD
new file mode 100644
index 00000000000..87f3753e491
--- /dev/null
+++ b/testprojects/src/java/org/pantsbuild/testproject/shading/BUILD
@@ -0,0 +1,52 @@
+# Copyright 2015 Pants project contributors (see CONTRIBUTORS.md).
+# Licensed under the Apache License, Version 2.0 (see LICENSE).
+
+jvm_binary(name='shading',
+ main='org.pantsbuild.testproject.shading.Main',
+ basename='shading',
+ dependencies=[
+ ':lib',
+ 'testprojects/src/java/org/pantsbuild/testproject/shadingdep',
+ 'testprojects/src/java/org/pantsbuild/testproject/shadingdep:other',
+ ],
+ shading_rules=[
+ shading_exclude('org.pantsbuild.testproject.shadingdep.PleaseDoNotShadeMe'),
+ shading_relocate('org.pantsbuild.testproject.shadingdep.otherpackage.ShadeWithTargetId'),
+ shading_relocate('org.pantsbuild.testproject.shadingdep.CompletelyRename',
+ 'org.pantsbuild.testproject.foo.bar.MyNameIsDifferentNow'),
+ shading_relocate_package('org.pantsbuild.testproject.shadingdep'),
+ shading_relocate('org.pantsbuild.testproject.shading.ShadeSelf')
+ ],
+)
+
+java_library(name='lib',
+ sources=[
+ 'Main.java',
+ 'ShadeSelf.java',
+ ],
+)
+
+java_library(name='third_lib',
+ sources=[
+ 'Third.java',
+ 'Second.java',
+ ],
+ platform='java7',
+ dependencies=[
+ '3rdparty:gson',
+ ],
+)
+
+jvm_binary(name='third',
+ basename='third',
+ main='org.pantsbuild.testproject.shading.Third',
+ platform='java7',
+ dependencies=[
+ ':third_lib',
+ ],
+ shading_rules=[
+ shading_exclude('org.pantsbuild.testproject.shading.Third'),
+ shading_relocate_package('org.pantsbuild', shade_prefix='hello.'),
+ shading_relocate('com.google.gson.**', shade_pattern='moc.elgoog.nosg.@1'),
+ ],
+)
diff --git a/testprojects/src/java/org/pantsbuild/testproject/shading/Main.java b/testprojects/src/java/org/pantsbuild/testproject/shading/Main.java
new file mode 100644
index 00000000000..ffbb0b176bd
--- /dev/null
+++ b/testprojects/src/java/org/pantsbuild/testproject/shading/Main.java
@@ -0,0 +1,19 @@
+// Copyright 2015 Pants project contributors (see CONTRIBUTORS.md).
+// Licensed under the Apache License, Version 2.0 (see LICENSE).
+
+package org.pantsbuild.testproject.shading;
+
+import org.pantsbuild.testproject.shadingdep.PleaseDoNotShadeMe;
+import org.pantsbuild.testproject.shadingdep.otherpackage.ShadeWithTargetId;
+
+/**
+ * Used to test shading support in tests/python/pants_test/java/jar/test_shader_integration.py
+ */
+public class Main {
+ public static void main(String[] args) {
+ System.out.println("Hello, shady world!");
+ ShadeWithTargetId.main(args);
+ PleaseDoNotShadeMe.sayPlease();
+ ShadeSelf.sayHi();
+ }
+}
diff --git a/testprojects/src/java/org/pantsbuild/testproject/shading/Second.java b/testprojects/src/java/org/pantsbuild/testproject/shading/Second.java
new file mode 100644
index 00000000000..f3153047591
--- /dev/null
+++ b/testprojects/src/java/org/pantsbuild/testproject/shading/Second.java
@@ -0,0 +1,21 @@
+// Copyright 2015 Pants project contributors (see CONTRIBUTORS.md).
+// Licensed under the Apache License, Version 2.0 (see LICENSE).
+
+package org.pantsbuild.testproject.shading;
+
+/**
+ * This is the not-main file in a tiny library made to test shading of 3rdparty libraries.
+ */
+public class Second {
+
+ private int writeOnlyInteger = 0;
+
+ /**
+ * The behavior of this function is irrelevant, we just want to make sure it can be called at all.
+ * @param i
+ */
+ public void write(int i) {
+ this.writeOnlyInteger = i;
+ }
+
+}
diff --git a/testprojects/src/java/org/pantsbuild/testproject/shading/ShadeSelf.java b/testprojects/src/java/org/pantsbuild/testproject/shading/ShadeSelf.java
new file mode 100644
index 00000000000..683d34704e3
--- /dev/null
+++ b/testprojects/src/java/org/pantsbuild/testproject/shading/ShadeSelf.java
@@ -0,0 +1,13 @@
+// Copyright 2015 Pants project contributors (see CONTRIBUTORS.md).
+// Licensed under the Apache License, Version 2.0 (see LICENSE).
+
+package org.pantsbuild.testproject.shading;
+
+/**
+ * Used to test shading support in tests/python/pants_test/java/jar/test_shader_integration.py
+ */
+public class ShadeSelf {
+ public static void sayHi() {
+ System.out.println("ShadeSelf says hi: " + ShadeSelf.class.getName());
+ }
+}
diff --git a/testprojects/src/java/org/pantsbuild/testproject/shading/Third.java b/testprojects/src/java/org/pantsbuild/testproject/shading/Third.java
new file mode 100644
index 00000000000..2e7406bd946
--- /dev/null
+++ b/testprojects/src/java/org/pantsbuild/testproject/shading/Third.java
@@ -0,0 +1,29 @@
+// Copyright 2015 Pants project contributors (see CONTRIBUTORS.md).
+// Licensed under the Apache License, Version 2.0 (see LICENSE).
+
+package org.pantsbuild.testproject.shading;
+
+import java.util.Map;
+import java.util.HashMap;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+
+/**
+ * This is the main file in a tiny library just used to test 3rdparty dependency shading.
+ */
+public class Third {
+
+ public static void main(String[] args) {
+ Map classNames = new HashMap<>();
+ classNames.put(Third.class.getSimpleName(), Third.class.getName());
+ classNames.put(Second.class.getSimpleName(), Second.class.getName());
+ classNames.put(Gson.class.getSimpleName(), Gson.class.getName());
+
+ new Second().write(42);
+
+ Gson gson = new GsonBuilder().create();
+ System.out.println(gson.toJson(classNames));
+ }
+
+}
diff --git a/testprojects/src/java/org/pantsbuild/testproject/shadingdep/BUILD b/testprojects/src/java/org/pantsbuild/testproject/shadingdep/BUILD
new file mode 100644
index 00000000000..a67b9187ee2
--- /dev/null
+++ b/testprojects/src/java/org/pantsbuild/testproject/shadingdep/BUILD
@@ -0,0 +1,23 @@
+# Copyright 2015 Pants project contributors (see CONTRIBUTORS.md).
+# Licensed under the Apache License, Version 2.0 (see LICENSE).
+
+jvm_binary(name='shadingdep',
+ main='org.pantsbuild.testproject.shadingdep.Dependency',
+ basename='shadingdep',
+ dependencies=[
+ ':lib',
+ 'testprojects/src/java/org/pantsbuild/testproject/shadingdep/subpackage',
+ ],
+)
+
+jvm_binary(name='other',
+ main='org.pantsbuild.testproject.shadingdep.otherpackage.ShadeWithTargetId',
+ basename='other',
+ dependencies=[
+ 'testprojects/src/java/org/pantsbuild/testproject/shadingdep/otherpackage',
+ ],
+)
+
+java_library(name='lib',
+ sources=globs('*.java'),
+)
diff --git a/testprojects/src/java/org/pantsbuild/testproject/shadingdep/CompletelyRename.java b/testprojects/src/java/org/pantsbuild/testproject/shadingdep/CompletelyRename.java
new file mode 100644
index 00000000000..cb8130a024f
--- /dev/null
+++ b/testprojects/src/java/org/pantsbuild/testproject/shadingdep/CompletelyRename.java
@@ -0,0 +1,16 @@
+// Copyright 2015 Pants project contributors (see CONTRIBUTORS.md).
+// Licensed under the Apache License, Version 2.0 (see LICENSE).
+
+package org.pantsbuild.testproject.shadingdep;
+
+/**
+ * Used to test shading support in tests/python/pants_test/java/jar/test_shader_integration.py
+ */
+public class CompletelyRename {
+
+ public static void main(String[] args) {
+ System.out.println("CompletelyRename was completely renamed to "
+ + CompletelyRename.class.getName());
+ }
+
+}
diff --git a/testprojects/src/java/org/pantsbuild/testproject/shadingdep/Dependency.java b/testprojects/src/java/org/pantsbuild/testproject/shadingdep/Dependency.java
new file mode 100644
index 00000000000..8a2c7a40ac5
--- /dev/null
+++ b/testprojects/src/java/org/pantsbuild/testproject/shadingdep/Dependency.java
@@ -0,0 +1,11 @@
+// Copyright 2015 Pants project contributors (see CONTRIBUTORS.md).
+// Licensed under the Apache License, Version 2.0 (see LICENSE).
+
+package org.pantsbuild.testproject.shadingdep;
+
+/**
+ * Used to test shading support in tests/python/pants_test/java/jar/test_shader_integration.py
+ */
+public class Dependency {
+ public static void main(String[] args) {}
+}
diff --git a/testprojects/src/java/org/pantsbuild/testproject/shadingdep/PleaseDoNotShadeMe.java b/testprojects/src/java/org/pantsbuild/testproject/shadingdep/PleaseDoNotShadeMe.java
new file mode 100644
index 00000000000..f13666131e8
--- /dev/null
+++ b/testprojects/src/java/org/pantsbuild/testproject/shadingdep/PleaseDoNotShadeMe.java
@@ -0,0 +1,13 @@
+// Copyright 2015 Pants project contributors (see CONTRIBUTORS.md).
+// Licensed under the Apache License, Version 2.0 (see LICENSE).
+
+package org.pantsbuild.testproject.shadingdep;
+
+/**
+ * Used to test shading support in tests/python/pants_test/java/jar/test_shader_integration.py
+ */
+public class PleaseDoNotShadeMe {
+ public static void sayPlease() {
+ System.out.println("PleaseDoNotShadeMe: " + PleaseDoNotShadeMe.class.getName());
+ }
+}
diff --git a/testprojects/src/java/org/pantsbuild/testproject/shadingdep/SomeClass.java b/testprojects/src/java/org/pantsbuild/testproject/shadingdep/SomeClass.java
new file mode 100644
index 00000000000..e9b1a4d7098
--- /dev/null
+++ b/testprojects/src/java/org/pantsbuild/testproject/shadingdep/SomeClass.java
@@ -0,0 +1,9 @@
+// Copyright 2015 Pants project contributors (see CONTRIBUTORS.md).
+// Licensed under the Apache License, Version 2.0 (see LICENSE).
+
+package org.pantsbuild.testproject.shadingdep;
+
+/**
+ * Used to test shading support in tests/python/pants_test/java/jar/test_shader_integration.py
+ */
+public class SomeClass {}
diff --git a/testprojects/src/java/org/pantsbuild/testproject/shadingdep/otherpackage/BUILD b/testprojects/src/java/org/pantsbuild/testproject/shadingdep/otherpackage/BUILD
new file mode 100644
index 00000000000..1cd09aa4ac2
--- /dev/null
+++ b/testprojects/src/java/org/pantsbuild/testproject/shadingdep/otherpackage/BUILD
@@ -0,0 +1,6 @@
+# Copyright 2015 Pants project contributors (see CONTRIBUTORS.md).
+# Licensed under the Apache License, Version 2.0 (see LICENSE).
+
+java_library(name='otherpackage',
+ sources=globs('*.java'),
+)
diff --git a/testprojects/src/java/org/pantsbuild/testproject/shadingdep/otherpackage/ShadeWithTargetId.java b/testprojects/src/java/org/pantsbuild/testproject/shadingdep/otherpackage/ShadeWithTargetId.java
new file mode 100644
index 00000000000..f12ab1762ca
--- /dev/null
+++ b/testprojects/src/java/org/pantsbuild/testproject/shadingdep/otherpackage/ShadeWithTargetId.java
@@ -0,0 +1,13 @@
+// Copyright 2015 Pants project contributors (see CONTRIBUTORS.md).
+// Licensed under the Apache License, Version 2.0 (see LICENSE).
+
+package org.pantsbuild.testproject.shadingdep.otherpackage;
+
+/**
+ * Used to test shading support in tests/python/pants_test/java/jar/test_shader_integration.py
+ */
+public class ShadeWithTargetId {
+ public static void main(String[] args) {
+ System.out.println("ShadeWithTargetId: " + ShadeWithTargetId.class.getName());
+ }
+}
diff --git a/testprojects/src/java/org/pantsbuild/testproject/shadingdep/subpackage/BUILD b/testprojects/src/java/org/pantsbuild/testproject/shadingdep/subpackage/BUILD
new file mode 100644
index 00000000000..3ccfcf11d6e
--- /dev/null
+++ b/testprojects/src/java/org/pantsbuild/testproject/shadingdep/subpackage/BUILD
@@ -0,0 +1,6 @@
+# Copyright 2015 Pants project contributors (see CONTRIBUTORS.md).
+# Licensed under the Apache License, Version 2.0 (see LICENSE).
+
+java_library(name='subpackage',
+ sources=globs('*.java'),
+)
diff --git a/testprojects/src/java/org/pantsbuild/testproject/shadingdep/subpackage/Subpackaged.java b/testprojects/src/java/org/pantsbuild/testproject/shadingdep/subpackage/Subpackaged.java
new file mode 100644
index 00000000000..c0d6c78bc15
--- /dev/null
+++ b/testprojects/src/java/org/pantsbuild/testproject/shadingdep/subpackage/Subpackaged.java
@@ -0,0 +1,10 @@
+// Copyright 2015 Pants project contributors (see CONTRIBUTORS.md).
+// Licensed under the Apache License, Version 2.0 (see LICENSE).
+
+package org.pantsbuild.testproject.shadingdep.subpackage;
+
+/**
+ * Used to test shading support in tests/python/pants_test/java/jar/test_shader_integration.py
+ */
+public class Subpackaged {
+}
diff --git a/tests/python/pants_test/backend/jvm/tasks/test_bootstrap_jvm_tools.py b/tests/python/pants_test/backend/jvm/tasks/test_bootstrap_jvm_tools.py
index a68ad0558f7..9a4a12bb62b 100644
--- a/tests/python/pants_test/backend/jvm/tasks/test_bootstrap_jvm_tools.py
+++ b/tests/python/pants_test/backend/jvm/tasks/test_bootstrap_jvm_tools.py
@@ -14,7 +14,7 @@
from pants.backend.jvm.tasks.jvm_tool_task_mixin import JvmToolTaskMixin
from pants.java.distribution.distribution import DistributionLocator
from pants.java.executor import SubprocessExecutor
-from pants.java.jar.shader import Shader
+from pants.java.jar.shader import Shading
from pants.util.contextutil import open_zip
from pants_test.jvm.jvm_tool_task_test_base import JvmToolTaskTestBase
from pants_test.subsystem.subsystem_util import subsystem_instance
@@ -86,7 +86,7 @@ def classfile_contents(classpath):
for excluded_class in excluded_classes:
self.assertEqual('org/apache/tools/ant', os.path.dirname(excluded_class))
- prefix_len = len(Shader.SHADE_PREFIX)
+ prefix_len = len(Shading.SHADE_PREFIX)
def strip_prefix(shaded):
return set(classfile[prefix_len:] for classfile in shaded)
diff --git a/tests/python/pants_test/java/jar/BUILD b/tests/python/pants_test/java/jar/BUILD
index 323f2479ee9..4af14494abc 100644
--- a/tests/python/pants_test/java/jar/BUILD
+++ b/tests/python/pants_test/java/jar/BUILD
@@ -28,3 +28,15 @@ python_tests(
'src/python/pants/util:dirutil',
]
)
+
+python_tests(
+ name='shader_integration',
+ sources=['test_shader_integration.py'],
+ dependencies=[
+ 'src/python/pants/fs',
+ 'src/python/pants/util:contextutil',
+ 'src/python/pants/java/distribution',
+ 'tests/python/pants_test:int-test',
+ 'tests/python/pants_test/subsystem:subsystem_utils',
+ ],
+)
diff --git a/tests/python/pants_test/java/jar/test_shader.py b/tests/python/pants_test/java/jar/test_shader.py
index edc4ef9ab04..032a9f761f6 100644
--- a/tests/python/pants_test/java/jar/test_shader.py
+++ b/tests/python/pants_test/java/jar/test_shader.py
@@ -11,7 +11,7 @@
from pants.java.distribution.distribution import DistributionLocator
from pants.java.executor import SubprocessExecutor
-from pants.java.jar.shader import Shader
+from pants.java.jar.shader import Shader, Shading
from pants.util.contextutil import open_zip
from pants.util.dirutil import safe_delete
from pants_test.subsystem.subsystem_util import subsystem_instance
@@ -93,7 +93,72 @@ def test_runner_command(self):
self.assertEqual('rule * @1', lines[1]) # Exclude main's package.
self.assertIn('rule javax.annotation.* javax.annotation.@1', lines) # Exclude system.
self.assertEqual('rule com.google.common.base.* {}com.google.common.base.@1'
- .format(Shader.SHADE_PREFIX), lines[-1]) # Shade the rest.
+ .format(Shading.SHADE_PREFIX), lines[-1]) # Shade the rest.
self.assertEqual(input_jar, command.pop(0))
self.assertEqual(self.output_jar, command.pop(0))
+
+ def test_sanitize_package_name(self):
+ def assert_sanitize(name, sanitized):
+ self.assertEqual(sanitized, Shading.Relocate._sanitize_package_name(name))
+
+ assert_sanitize('hello', 'hello')
+ assert_sanitize('hello.goodbye', 'hello.goodbye')
+ assert_sanitize('.hello.goodbye', 'hello.goodbye')
+ assert_sanitize('hello.goodbye.', 'hello.goodbye')
+ assert_sanitize('123', '_123')
+ assert_sanitize('123.456', '_123._456')
+ assert_sanitize('123.v2', '_123.v2')
+ assert_sanitize('hello-goodbye', 'hello_goodbye')
+ assert_sanitize('hello-/.goodbye.?', 'hello__.goodbye._')
+ assert_sanitize('one.two..three....four.', 'one.two.three.four')
+
+ def test_infer_shaded_pattern(self):
+ def assert_inference(from_pattern, prefix, to_pattern):
+ result = ''.join(Shading.Relocate._infer_shaded_pattern_iter(from_pattern, prefix))
+ self.assertEqual(to_pattern, result)
+
+ assert_inference('com.foo.bar.Main', None, 'com.foo.bar.Main')
+ assert_inference('com.foo.bar.', None, 'com.foo.bar.')
+ assert_inference('com.foo.bar.', '__prefix__.', '__prefix__.com.foo.bar.')
+ assert_inference('com.*.bar.', None, 'com.@1.bar.')
+ assert_inference('com.*.bar.*.', None, 'com.@1.bar.@2.')
+ assert_inference('com.*.bar.**', None, 'com.@1.bar.@2')
+ assert_inference('*', None, '@1')
+ assert_inference('**', None, '@1')
+ assert_inference('**', '__prefix__.', '__prefix__.@1')
+
+ def test_shading_exclude(self):
+ def assert_exclude(from_pattern, to_pattern):
+ self.assertEqual((from_pattern, to_pattern), Shading.Exclude.new(from_pattern).rule())
+
+ assert_exclude('com.foo.bar.Main', 'com.foo.bar.Main')
+ assert_exclude('com.foo.bar.**', 'com.foo.bar.@1')
+ assert_exclude('com.*.bar.**', 'com.@1.bar.@2')
+
+ def test_shading_exclude_package(self):
+ self.assertEqual(('com.foo.bar.**', 'com.foo.bar.@1'),
+ Shading.ExcludePackage.new('com.foo.bar').rule())
+ self.assertEqual(('com.foo.bar.*', 'com.foo.bar.@1'),
+ Shading.ExcludePackage.new('com.foo.bar', recursive=False).rule())
+
+ def test_relocate(self):
+ self.assertEqual(('com.foo.bar.**', '{}com.foo.bar.@1'.format(Shading.SHADE_PREFIX)),
+ Shading.Relocate.new(from_pattern='com.foo.bar.**').rule())
+
+ self.assertEqual(('com.foo.bar.**', '{}com.foo.bar.@1'.format('__my_prefix__.')),
+ Shading.Relocate.new(from_pattern='com.foo.bar.**',
+ shade_prefix='__my_prefix__.').rule())
+
+ self.assertEqual(('com.foo.bar.**', 'org.biz.baz.@1'.format('__my_prefix__.')),
+ Shading.Relocate.new(from_pattern='com.foo.bar.**',
+ shade_prefix='__my_prefix__.',
+ shade_pattern='org.biz.baz.@1').rule())
+
+ def test_relocate_package(self):
+ self.assertEqual(('com.foo.bar.**', '{}com.foo.bar.@1'.format(Shading.SHADE_PREFIX)),
+ Shading.RelocatePackage.new('com.foo.bar').rule())
+ self.assertEqual(('com.foo.bar.*', '{}com.foo.bar.@1'.format(Shading.SHADE_PREFIX)),
+ Shading.RelocatePackage.new('com.foo.bar', recursive=False).rule())
+ self.assertEqual(('com.foo.bar.**', '__p__.com.foo.bar.@1'),
+ Shading.RelocatePackage.new('com.foo.bar', shade_prefix='__p__.').rule())
diff --git a/tests/python/pants_test/java/jar/test_shader_integration.py b/tests/python/pants_test/java/jar/test_shader_integration.py
new file mode 100644
index 00000000000..e5ecc14b942
--- /dev/null
+++ b/tests/python/pants_test/java/jar/test_shader_integration.py
@@ -0,0 +1,111 @@
+# coding=utf-8
+# Copyright 2015 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)
+
+import json
+import os
+import subprocess
+from contextlib import contextmanager
+
+from pants.fs.archive import ZIP
+from pants.java.distribution.distribution import DistributionLocator
+from pants.java.executor import SubprocessExecutor
+from pants.util.contextutil import temporary_dir
+from pants_test.pants_run_integration_test import PantsRunIntegrationTest
+from pants_test.subsystem.subsystem_util import subsystem_instance
+
+
+class ShaderIntegrationTest(PantsRunIntegrationTest):
+
+ def test_shader_project(self):
+ """Test that the binary target at the ``shading_project`` can be built and run.
+
+ Explicitly checks that the classes end up with the correct shaded fully qualified classnames.
+ """
+ shading_project = 'testprojects/src/java/org/pantsbuild/testproject/shading'
+ self.assert_success(self.run_pants(['clean-all']))
+ self.assert_success(self.run_pants(['binary', shading_project]))
+
+ expected_classes = {
+ # Explicitly excluded by a shading_exclude() rule.
+ 'org/pantsbuild/testproject/shadingdep/PleaseDoNotShadeMe.class',
+ # Not matched by any rule, so stays the same.
+ 'org/pantsbuild/testproject/shading/Main.class',
+ # Shaded with the target_id prefix, along with the default pants prefix.
+ ('__shaded_by_pants__/org/pantsbuild/testproject/shadingdep/otherpackage/'
+ 'ShadeWithTargetId.class'),
+ # Also shaded with the target_id prefix and default pants prefix, but for a different target
+ # (so the target_id is different).
+ ('__shaded_by_pants__/org/pantsbuild/testproject/shading/ShadeSelf.class'),
+ # All these are shaded by the same shading_relocate_package(), which is recursive by default.
+ '__shaded_by_pants__/org/pantsbuild/testproject/shadingdep/subpackage/Subpackaged.class',
+ '__shaded_by_pants__/org/pantsbuild/testproject/shadingdep/SomeClass.class',
+ '__shaded_by_pants__/org/pantsbuild/testproject/shadingdep/Dependency.class',
+ # Shaded by a shading_relocate() that completely renames the package and class name.
+ 'org/pantsbuild/testproject/foo/bar/MyNameIsDifferentNow.class',
+ }
+
+ path = os.path.join('dist', 'shading.jar')
+ with subsystem_instance(DistributionLocator):
+ execute_java = DistributionLocator.cached(minimum_version='1.6').execute_java
+ self.assertEquals(0, execute_java(classpath=path,
+ main='org.pantsbuild.testproject.shading.Main'))
+ self.assertEquals(0, execute_java(classpath=path,
+ main='org.pantsbuild.testproject.foo.bar.MyNameIsDifferentNow'))
+
+ received_classes = set()
+ with temporary_dir() as tempdir:
+ ZIP.extract(path, tempdir, filter_func=lambda f: f.endswith('.class'))
+ for root, dirs, files in os.walk(tempdir):
+ for name in files:
+ received_classes.add(os.path.relpath(os.path.join(root, name), tempdir))
+
+ self.assertEqual(expected_classes, received_classes)
+
+ def _bundle_and_run(self, bundle_args, classpath):
+ self.assert_success(self.run_pants(['clean-all']))
+ pants_command = list(bundle_args)
+ pants_command.append('testprojects/src/java/org/pantsbuild/testproject/shading:third')
+ self.assert_success(self.run_pants(pants_command))
+
+ main_class = 'org.pantsbuild.testproject.shading.Third'
+ with subsystem_instance(DistributionLocator):
+ executor = SubprocessExecutor(DistributionLocator.cached(minimum_version='1.7'))
+ p = executor.spawn(classpath, main_class, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ out, err = p.communicate()
+ self.assertEqual(0, p.returncode, err)
+ class_names = json.loads(out.strip())
+ self.assertEqual({
+ 'Gson': 'moc.elgoog.nosg.Gson',
+ 'Third': 'org.pantsbuild.testproject.shading.Third',
+ 'Second': 'hello.org.pantsbuild.testproject.shading.Second',
+ }, class_names)
+
+ @contextmanager
+ def _dist_dir(self):
+ with temporary_dir(root_dir=os.path.abspath('.')) as dist_dir:
+ yield os.path.relpath(dist_dir)
+
+ def test_no_deployjar_run(self):
+ with self._dist_dir() as dist_dir:
+ bundle_args = [
+ '--pants-distdir={}'.format(dist_dir), 'bundle', '--no-deployjar',
+ ]
+ classpath=[
+ os.path.join(dist_dir, 'third-bundle', 'third.jar'),
+ os.path.join(dist_dir, 'third-bundle', 'libs'),
+ ]
+ self._bundle_and_run(bundle_args, classpath)
+
+ def test_deployjar_run(self):
+ with self._dist_dir() as dist_dir:
+ bundle_args = [
+ '--pants-distdir={}'.format(dist_dir), 'bundle', '--deployjar',
+ ]
+ classpath=[
+ os.path.join(dist_dir, 'third-bundle', 'third.jar'),
+ ]
+ self._bundle_and_run(bundle_args, classpath)