Skip to content

Commit

Permalink
Adds the ability to specify explicit fields in MANIFEST.MF in a jvm_b…
Browse files Browse the repository at this point in the history
…inary target.

- Added 'manifest_entries' to jvm_binary target
- Added a query to see if the manifest is empty in Manifest
- Support detecting the git workdir from a directory other than cwd
- Allow setting of both a main entry point and a custom manifest file when invoking jar-tool
- Special 'git' value for Implementation-Version pulls the current git commit id for the directory where the jvm_binary() target is defined.

Testing Done:
Updated tests.
Modified the `hello/simple/BUILD` file

```
 jvm_binary(name = 'simple',
   source = 'HelloWorld.java',
-  main = 'org.pantsbuild.example.hello.simple.HelloWorld',
+  #main = 'org.pantsbuild.example.hello.simple.HelloWorld',
+  manifest_entries = {
+    'Main-Class' : 'org.pantsbuild.example.hello.simple.HelloWorld',
+    'Foo' : 'foo',
+    'Implementation-Version' : 'git',
+  }
 )
```

Then ran `./pants binary examples/src/java/org/pantsbuild/example/hello/simple/` and inspected the MANIFEST.MF file:

```
Manifest-Version: 1.0
Implementation-Version: 748d4da
Foo: foo
Created-By: com.twitter.common.jar.tool.JarBuilder
Main-Class: org.pantsbuild.example.hello.simple.HelloWorld

```

Bugs closed: 1402

Reviewed at https://rbcommons.com/s/twitter/r/2084/
  • Loading branch information
ericzundel committed Apr 21, 2015
1 parent 937907d commit 40a4a5b
Show file tree
Hide file tree
Showing 16 changed files with 325 additions and 59 deletions.
46 changes: 45 additions & 1 deletion src/python/pants/backend/jvm/targets/jvm_binary.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,6 @@ def skip_signatures_and_duplicates_concat_well_known_metadata(cls, default_dup_a
has the following special handling:
- jar signature metadata is dropped
- ``java.util.ServiceLoader`` provider-configuration files are concatenated in the order
- ``java.util.ServiceLoader`` provider-configuration files are concatenated in the order
encountered
Expand Down Expand Up @@ -232,6 +231,39 @@ def value(self):
return self._jar_rules


class ManifestEntries(FingerprintedMixin):
"""Describes additional items to add to the app manifest."""

class ExpectedDictionaryError(Exception):
pass

def __init__(self, entries=None):
"""
:param entries: Additional headers, value pairs to add to the MANIFEST.MF.
You can just add fixed string header / value pairs.
:type entries: dictionary of string : string
"""
self.payload = Payload()
if entries:
if not isinstance(entries, dict):
raise self.ExpectedDictionaryError("entries must be a dictionary of strings.")
for key in entries.keys():
if not isinstance(key, string_types):
raise self.ExpectedDictionaryError(
"entries must be dictionary of strings, got key {} type {}"
.format(key, type(key).__name__))
self.payload.add_fields({
'entries' : PrimitiveField(entries or {}),
})

def fingerprint(self):
return self.payload.fingerprint()

@property
def entries(self):
return self.payload.entries


class JvmBinary(JvmTarget):
"""Produces a JVM binary optionally identifying a launcher main class.
Expand All @@ -252,6 +284,7 @@ def __init__(self,
source=None,
deploy_excludes=None,
deploy_jar_rules=None,
manifest_entries=None,
**kwargs):
"""
:param string main: The name of the ``main`` class, e.g.,
Expand All @@ -275,6 +308,8 @@ def __init__(self,
code but exclude the conflicting ``jar`` when deploying.
:param deploy_jar_rules: `Jar rules <#jar_rules>`_ for packaging this binary in a
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
Expand All @@ -288,12 +323,17 @@ def __init__(self,
raise TargetDefinitionException(self,
'deploy_jar_rules must be a JarRules specification. got {}'
.format(type(deploy_jar_rules).__name__))
if manifest_entries and not isinstance(manifest_entries, dict):
raise TargetDefinitionException(self,
'manifest_entries must be a dict. got {}'
.format(type(manifest_entries).__name__))
sources = [source] if source else None
payload = payload or Payload()
payload.add_fields({
'basename' : PrimitiveField(basename or name),
'deploy_excludes' : ExcludesField(self.assert_list(deploy_excludes, expected_type=Exclude)),
'deploy_jar_rules' : FingerprintedField(deploy_jar_rules or JarRules.default()),
'manifest_entries' : FingerprintedField(ManifestEntries(manifest_entries)),
'main': PrimitiveField(main),
})

Expand All @@ -318,3 +358,7 @@ def deploy_jar_rules(self):
@property
def main(self):
return self.payload.main

@property
def manifest_entries(self):
return self.payload.manifest_entries
1 change: 1 addition & 0 deletions src/python/pants/backend/jvm/tasks/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ python_library(
'3rdparty/python/twitter/commons:twitter.common.collections',
'src/python/pants/backend/jvm/subsystems:jar_tool',
'src/python/pants/backend/jvm/targets:java',
'src/python/pants/base:build_environment',
'src/python/pants/base:exceptions',
'src/python/pants/java/jar:manifest',
'src/python/pants/util:contextutil',
Expand Down
6 changes: 3 additions & 3 deletions src/python/pants/backend/jvm/tasks/jar_create.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,9 @@ def execute(self):
jar_name = jarname(target)
jar_path = os.path.join(self.workdir, jar_name)
with self.create_jar(target, jar_path) as jarfile:
jar_builder = self.create_jar_builder(jarfile)
if target in jar_builder.add_target(target):
self.context.products.get('jars').add(target, self.workdir).append(jar_name)
with self.create_jar_builder(jarfile) as jar_builder:
if target in jar_builder.add_target(target):
self.context.products.get('jars').add(target, self.workdir).append(jar_name)

@contextmanager
def create_jar(self, target, path):
Expand Down
62 changes: 51 additions & 11 deletions src/python/pants/backend/jvm/tasks/jar_task.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@

from pants.backend.jvm.subsystems.jar_tool import JarTool
from pants.backend.jvm.targets.java_agent import JavaAgent
from pants.backend.jvm.targets.jvm_binary import Duplicate, JarRules, Skip
from pants.backend.jvm.targets.jvm_binary import Duplicate, JarRules, JvmBinary, Skip
from pants.backend.jvm.tasks.nailgun_task import NailgunTask
from pants.base.build_environment import get_buildroot
from pants.base.exceptions import TaskError
from pants.binary_util import safe_args
from pants.java.jar.manifest import Manifest
Expand Down Expand Up @@ -84,7 +85,7 @@ def materialize(self, scratch_dir):
def __init__(self):
self._entries = []
self._jars = []
self._manifest = None
self._manifest_entry = None
self._main = None
self._classpath = None

Expand Down Expand Up @@ -143,7 +144,7 @@ def writestr(self, path, contents):

def _add_entry(self, entry):
if Manifest.PATH == entry.dest:
self._manifest = entry
self._manifest_entry = entry
else:
self._entries.append(entry)

Expand Down Expand Up @@ -179,14 +180,27 @@ def as_cli_entry(entry):
with safe_args(files, options, delimiter=',') as files_args:
with safe_args(jars, options, delimiter=',') as jars_args:

if self._main:
# If you specify --manifest to jar-tool you cannot specify --main.
if self._manifest_entry:
manifest_file = self._manifest_entry.materialize(manifest_stage_dir)
else:
manifest_file = None

if self._main and manifest_file:
main_arg = None
with open(manifest_file, 'a') as f:
f.write("Main-Class: {}\n".format(self._main))
else:
main_arg = self._main

if main_arg:
args.append('-main={}'.format(self._main))

if classpath_args:
args.append('-classpath={}'.format(','.join(classpath_args)))

if self._manifest:
args.append('-manifest={}'.format(self._manifest.materialize(manifest_stage_dir)))
if manifest_file:
args.append('-manifest={}'.format(manifest_file))

if files_args:
args.append('-files={}'.format(','.join(files_args)))
Expand Down Expand Up @@ -228,6 +242,7 @@ def _action_name(cls, action):
def __init__(self, *args, **kwargs):
super(JarTask, self).__init__(*args, **kwargs)
self.set_distribution(jdk=True)

# TODO(John Sirois): Consider poking a hole for custom jar-tool jvm args - namely for Xmx
# control.

Expand Down Expand Up @@ -281,9 +296,8 @@ class JarBuilder(AbstractClass):
"""A utility to aid in adding the classes and resources associated with targets to a jar."""

@staticmethod
def _write_agent_manifest(agent, jar):
def _add_agent_manifest(agent, manifest):
# TODO(John Sirois): refactor an agent model to support 'Boot-Class-Path' properly.
manifest = Manifest()
manifest.addentry(Manifest.MANIFEST_VERSION, '1.0')
if agent.premain:
manifest.addentry('Premain-Class', agent.premain)
Expand All @@ -295,7 +309,16 @@ def _write_agent_manifest(agent, jar):
manifest.addentry('Can-Retransform-Classes', 'true')
if agent.can_set_native_method_prefix:
manifest.addentry('Can-Set-Native-Method-Prefix', 'true')
jar.writestr(Manifest.PATH, manifest.contents())

@staticmethod
def _add_manifest_entries(jvm_binary_target, manifest):
"""Add additional fields to MANIFEST.MF as declared in the ManifestEntries structure.
:param JvmBinary jvm_binary_target:
:param Manifest manifest:
"""
for header, value in jvm_binary_target.manifest_entries.entries.iteritems():
manifest.addentry(header, value)

@staticmethod
def prepare(round_manager):
Expand All @@ -313,6 +336,8 @@ def prepare(round_manager):
def __init__(self, context, jar):
self._context = context
self._jar = jar
self._manifest = Manifest()


def add_target(self, target, recursive=False):
"""Adds the classes and resources for a target to an open jar.
Expand Down Expand Up @@ -355,7 +380,10 @@ def add_products(target_products):
add_products(resources_target)

if isinstance(tgt, JavaAgent):
self._write_agent_manifest(tgt, self._jar)
self._add_agent_manifest(tgt, self._manifest)

if isinstance(tgt, JvmBinary):
self._add_manifest_entries(tgt, self._manifest)

if recursive:
target.walk(add_to_jar)
Expand All @@ -364,6 +392,16 @@ def add_products(target_products):

return targets_added

def commit_manifest(self, jar):
"""Updates the manifest in the jar being written to.
Typically done right before closing the .jar. This gives a chance for all targets to bundle
in their contributions to the manifest.
"""
if not self._manifest.is_empty():
jar.writestr(Manifest.PATH, self._manifest.contents())

@contextmanager
def create_jar_builder(self, jar):
"""Creates a ``JarTask.JarBuilder`` ready for use.
Expand All @@ -372,4 +410,6 @@ def create_jar_builder(self, jar):
:param jar: An opened ``pants.backend.jvm.tasks.jar_task.Jar`.
"""
return self.JarBuilder(self.context, jar)
builder = self.JarBuilder(self.context, jar)
yield builder
builder.commit_manifest(jar)
19 changes: 9 additions & 10 deletions src/python/pants/backend/jvm/tasks/jvm_binary_task.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,16 +69,15 @@ def monolithic_jar(self, binary, path, with_external_deps):
compressed=True) as jar:

with self.context.new_workunit(name='add-internal-classes'):
self.create_jar_builder(jar).add_target(binary, recursive=True)

if with_external_deps:
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)

yield jar
with self.create_jar_builder(jar) as jar_builder:
jar_builder.add_target(binary, recursive=True)
if with_external_deps:
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)
yield jar

def _mapped_dependencies(self, jardepmap, binary, confs):
# TODO(John Sirois): rework product mapping towards well known types
Expand Down
7 changes: 6 additions & 1 deletion src/python/pants/java/jar/manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,13 @@ def addentry(self, header, value):
raise ValueError('Header name must be 68 characters or less, given {}'.format(header))
if self._contents:
self._contents += '\n'
self._contents += '\n'.join(self._wrap('{}: {}'.format(header, value)))
self._contents += '\n'.join(self._wrap('{header}: {value}'.format(header=header, value=value)))

def contents(self):
padded = self._contents + '\n'
return padded.encode('ascii')

def is_empty(self):
if self._contents.strip():
return False
return True
1 change: 1 addition & 0 deletions src/python/pants/scm/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ python_library(
name = 'scm',
sources = ['scm.py'],
dependencies = [
'src/python/pants/util:contextutil',
'src/python/pants/util:meta',
],
)
Expand Down
9 changes: 7 additions & 2 deletions src/python/pants/scm/git.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,14 @@
import traceback

from pants.scm.scm import Scm
from pants.util.contextutil import pushd


class Git(Scm):
"""An Scm implementation backed by git."""

@classmethod
def detect_worktree(cls, binary='git'):
def detect_worktree(cls, binary='git', dir=None):
"""Detect the git working tree above cwd and return it; else, return None.
binary: The path to the git binary to use, 'git' by default.
Expand All @@ -25,7 +26,11 @@ def detect_worktree(cls, binary='git'):
# pants.base.build_environment.get_scm, encapsulate in a true factory method.
cmd = [binary, 'rev-parse', '--show-toplevel']
try:
process, out = cls._invoke(cmd)
if dir:
with pushd(dir):
process, out = cls._invoke(cmd)
else:
process, out = cls._invoke(cmd)
cls._check_result(cmd, process.returncode, raise_type=Scm.ScmException)
except Scm.ScmException:
return None
Expand Down
11 changes: 11 additions & 0 deletions testprojects/src/java/org/pantsbuild/testproject/manifest/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Copyright 2015 Pants project contributors (see CONTRIBUTORS.md).
# Licensed under the Apache License, Version 2.0 (see LICENSE).

jvm_binary(
source = 'Manifest.java',
name = 'manifest',
main = 'org.pantsbuild.testproject.manifest.Manifest',
manifest_entries = {
'Implementation-Version' : '1.2.3',
}
)
Original file line number Diff line number Diff line change
@@ -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.manifest;

public class Manifest {
public static void main(String args[]) {
System.out.println("Hello World! Version: "
+ Package.getPackage("org.pantsbuild.testproject.manifest").getImplementationVersion());
}
}
Loading

0 comments on commit 40a4a5b

Please sign in to comment.