How to override jar task's manifest inside a custom plugin

I’m writing a custom plugin and inside that I want to override the jar task’s manifest based on properties the user configures. It seems my issue has to do with lazy loading - its trying to evaluate my project extension properties eagerly (when the plugin is applied) vs lazily (when the jar task actually needs to execute). Any help on how to fix this?

class ProjectInformation {
 String projectName
 String artifactName
}
  class CustomJavaPlugin implements Plugin<Project> {
 @Override
 void apply(Project project) {
  project.apply plugin: 'java'
                project.extensions.create("projectInfo", ProjectInformation)
    project.tasks.withType(Jar) {
   manifest = project.manifest {
    from manifest {
     attributes(
       (java.util.jar.Attributes.Name.IMPLEMENTATION_TITLE.toString()): (project.hasProperty('BF_PROJECTNAME') ? project.getProperty('BF_PROJECTNAME') : project.projectInfo.projectName),
       (java.util.jar.Attributes.Name.IMPLEMENTATION_VERSION.toString()): project.hasProperty('BF_TAG') ? project.getProperty('BF_TAG') : 'UNCONTROLLED ARTIFACT',
       (java.util.jar.Attributes.Name.IMPLEMENTATION_VENDOR.toString()): 'MyCompany',
       'Created-By': System.getProperty('java.version') + ' (' + System.getProperty('java.vendor') + ')',
       'Built-With': "gradle-${project.getGradle().getGradleVersion()}, groovy-${GroovySystem.getVersion()}",
       'Build-Time': project.hasProperty('Current_Date') ? project.getProperty('Current_Date') : 'NOT FOR PRODUCTION USE',
       'Built-By': project.hasProperty('BF_USER') ? project.getProperty('BF_USER') : System.getProperty('user.name'),
       'Built-On': "${InetAddress.localHost.hostName}/${InetAddress.localHost.hostAddress}"
       )
    }
   }
  }
 }

If I just put some code into a build.gradle it seems to work (see code snippet below). My issue is with translating this logic into a custom plugin.

ext.sharedManifest = manifest {
 attributes(
   (java.util.jar.Attributes.Name.IMPLEMENTATION_TITLE.toString()): (project.hasProperty('BF_PROJECTNAME') ? project.getProperty('BF_PROJECTNAME') : "Some Project Name"),
   (java.util.jar.Attributes.Name.IMPLEMENTATION_VERSION.toString()): project.hasProperty('BF_TAG') ? project.getProperty('BF_TAG') : 'UNCONTROLLED ARTIFACT',
   (java.util.jar.Attributes.Name.IMPLEMENTATION_VENDOR.toString()): 'MyCompany',
   'Created-By': System.getProperty('java.version') + ' (' + System.getProperty('java.vendor') + ')',
   'Built-With': "gradle-${project.getGradle().getGradleVersion()}, groovy-${GroovySystem.getVersion()}",
   'Build-Time': project.hasProperty('Current_Date') ? project.getProperty('Current_Date') : 'NOT FOR PRODUCTION USE',
   'Built-By': project.hasProperty('BF_USER') ? project.getProperty('BF_USER') : System.getProperty('user.name'),
   'Built-On': "${InetAddress.localHost.hostName}/${InetAddress.localHost.hostAddress}"
   )
}
  jar {
 manifest = project.manifest {
  from sharedManifest
 }
}

I think you should put the plugin code inside a ‘project.afterEvaluate’ block.

In my multiproject build I have projects with an existing MANIFEST.MF file, others do not have it. So I do this, overriding the “jar” task:

jar {
   manifest {
  // benutze das im Projekt vorliegende File, falls vorhanden:
  def manif = "${projectDir}/META-INF/MANIFEST.MF"
       if (new File(manif).exists()) {
   from file(manif)
     }
  else {
   logger.info(project.name + " doesn't have a META-INF/MANIFEST.MF.")
     manifest.attributes provider: 'xyz,abc'
   manifest.attributes project: project.name
   manifest.attributes Build: new Date()
  }
 }
    }

Hope this helps.

Note that the methods on javadoc:org.gradle.api.java.archives.Manifest that you use to set attribute accept ‘Object’ for the value part. This is a common pattern used in Gradle to achieve laziness; the object will be converted to a ‘String’ at the latest possible moment by calling ‘toString()’ on it.

This means that you can set a value that is a calculation. You can either write your own class for this, or use Groovy’s Lazy GString functionality. See this blog post for some more on this topic.

Using a closure inside of the GString works like a charm. Great feature to know about. Also putting my code inside a project.afterEvaluate block works too, but I like the GString/closure better as I can scope my classes without having to think about “did I put the caller to my class inside an afterEvaluate?” Wherever I need these custom properties as strings I can just use the Closure idea.

I also spoke with Peter Niederwieser and he mentioned using the conventionMapping approach, so in my plugin I could do something like this

class CustomPlugin implements Plugin<Project> {
  @Override
   void apply(Project project) {
    project.tasks.withType(Jar) {
      conventionMapping.manifest = new DefaultManifest(new IdentityFileResolver()) {
        attributes(
            (java.util.jar.Attributes.Name.IMPLEMENTATION_TITLE.toString()): (project.hasProperty('BF_PROJECTNAME') ? project.getProperty('BF_PROJECTNAME') : project.someCustomExtensionObject.fieldInCustomExtensionObject),
            (java.util.jar.Attributes.Name.IMPLEMENTATION_VERSION.toString()): project.hasProperty('BF_TAG') ? project.getProperty('BF_TAG') : 'UNCONTROLLED ARTIFACT',
            (java.util.jar.Attributes.Name.IMPLEMENTATION_VENDOR.toString()): 'MyCompany',
            'Created-By': System.getProperty('java.version') + ' (' + System.getProperty('java.vendor') + ')',
            'Built-With': "gradle-${project.getGradle().getGradleVersion()}, groovy-${GroovySystem.getVersion()}",
            'Build-Time': project.hasProperty('Current_Date') ? project.getProperty('Current_Date') : 'NOT FOR PRODUCTION USE',
            'Built-By': project.hasProperty('BF_USER') ? project.getProperty('BF_USER') : System.getProperty('user.name'),
            'Built-On': "${InetAddress.localHost.hostName}/${InetAddress.localHost.hostAddress}"
            )
      }
    }
  }
}

What I actually decided on was this:

class CustomPlugin implements Plugin<Project> {
  @Override
   void apply(Project project) {
    project.tasks.withType(Jar) {
      manifest = new DefaultManifest(new IdentityFileResolver()) {
        attributes(
            (java.util.jar.Attributes.Name.IMPLEMENTATION_TITLE.toString()): (project.hasProperty('BF_PROJECTNAME') ? project.getProperty('BF_PROJECTNAME') : "${ -> project.someCustomExtensionObject.fieldInCustomExtensionObject}"),
            (java.util.jar.Attributes.Name.IMPLEMENTATION_VERSION.toString()): project.hasProperty('BF_TAG') ? project.getProperty('BF_TAG') : 'UNCONTROLLED ARTIFACT',
            (java.util.jar.Attributes.Name.IMPLEMENTATION_VENDOR.toString()): 'MyCompany',
            'Created-By': System.getProperty('java.version') + ' (' + System.getProperty('java.vendor') + ')',
            'Built-With': "gradle-${project.getGradle().getGradleVersion()}, groovy-${GroovySystem.getVersion()}",
            'Build-Time': project.hasProperty('Current_Date') ? project.getProperty('Current_Date') : 'NOT FOR PRODUCTION USE',
            'Built-By': project.hasProperty('BF_USER') ? project.getProperty('BF_USER') : System.getProperty('user.name'),
            'Built-On': "${InetAddress.localHost.hostName}/${InetAddress.localHost.hostAddress}"
            )
      }
    }
  }
}
1 Like

That’s how I’d do it to.

‘afterEvaluate’ is to be avoided if possible as it makes it difficult for users to override your plugin’s behaviour if they need to. And convention mappings should be avoided if possible for the time being because they aren’t part of the public API.

Thanks for the link to that article. The GString/closure syntax is a great (& powerful) feature to know about, especially as a plugin developer.

Seems this example is no more compileable. I’m observing

unexpected token: attributes @ line xx, column yy.

attributes (

What is correct solution for gradle 2.0?