gradlemanifest.mfgradle-dependenciesshadowjar

How to merge Manifest sections with Gradle and shadowJar


What I need

We package our products with Gradle and shadowJar. Some of the libraries we use, utilize individual sections in Jar Manifests, specifically attributes like Implementation-Title and Implementation-Version. These sometimes show in (the outputs of) our products, so I'd like them to survive the shawdowJar-Process.

Example

lib1.jar/META-INF/MANIFEST.MF

Manifest-Version: 1.0
...

Name: org/some/lib
...
Implementation-Title: someLib
Implementation-Version: 2.3
...

lib2.jar/META-INF/MANIFEST.MF

Manifest-Version: 1.0
...

Name: org/some/other/lib
...
Implementation-Title: someOtherLib
Implementation-Version: 5.7-RC
...

=> product.jar/META-INF/MANIFEST.MF

Manifest-Version: 1.0
...

Name: org/some/lib
...
Implementation-Title: someLib
Implementation-Version: 2.3
...

Name: org/some/other/lib
...
Implementation-Title: someOtherLib
Implementation-Version: 5.7-RC
...

What I found out

project.shadowJar {
    manifest {
        attributes(["Implementation-Title" : "someLib"], "org/some/lib")
        attributes(["Implementation-Title" : "someOtherLib"], "org/some/other/lib")
    }
}

generates exactly what I want, statically.

project.shadowJar {
    manifest {
        for (dependency in includedDependencies) {
            // read in jar file and set attributes
        }
    }
}

Gradle is not happy: "Cannot change dependencies of dependency configuration ':project:products:<ProductName>:compile' after it has been included in dependency resolution."

def dependencies = [];
project.tasks.register('resolveDependencies') {
    doFirst {
        gradleProject.configurations.compile.resolvedConfiguration.resolvedArtifacts.each {
            dependencies.add(it.file)
        }
    }
}
project.tasks['shadowJar'].dependsOn(project.tasks['resolveDependencies']);

project.shadowJar {
    manifest {
        // dependencies will be empty when this code is called
        for (dependency in dependencies) {
            // read in jar file and set attributes
        }
    }
}

The dependencies are not resolved in time.

What I'd like to know

How can I access the dependencies without upsetting Gradle? Alternatively, is there another way to merge the named individual sections with shadowJar?


Solution

  • According to https://github.com/johnrengelman/shadow/issues/369 the Transformer interface of shadowJar should be used to do this.

    So here comes:

    import com.github.jengelman.gradle.plugins.shadow.transformers.Transformer;
    import com.github.jengelman.gradle.plugins.shadow.transformers.TransformerContext;
    
    import java.io.ByteArrayOutputStream;
    import java.util.jar.Attributes;
    import java.util.jar.Manifest;
    import java.util.Map.Entry;
    
    import shadow.org.apache.tools.zip.ZipOutputStream;
    import shadow.org.apache.tools.zip.ZipEntry;
    import shadow.org.codehaus.plexus.util.IOUtil;
    
    import org.gradle.api.file.FileTreeElement;
    
    import static java.nio.charset.StandardCharsets.*
    import static java.util.jar.JarFile.*;
    
    /**ManifestVersionMergeTransformer appends all version information sections from manifest files to the resulting manifest file.
      * @author Robert Lichtenberger
      */
    public class ManifestMergeTransformer implements Transformer {
    
        String includePackages; // regular expression that must match a given package
        String excludePackages; // regular expression that must not match a given package
    
        private Manifest manifest;
    
        @Override
        public boolean canTransformResource(FileTreeElement element) {
            MANIFEST_NAME.equalsIgnoreCase(element.relativePath.pathString);
        }
    
        @Override
        public void transform(TransformerContext context) {
            if (manifest == null) {
                manifest = new Manifest(context.is);
            } else {
                Manifest toMerge = new Manifest(context.is);
                for (Entry<String, Attributes> entry : toMerge.getEntries().entrySet()) {
                    if (mustInclude(entry.getKey())) {
                        manifest.getEntries().put(entry.getKey(), entry.getValue());
                    }
                }
            }
            IOUtil.close(context.is);
        }
    
        private boolean mustInclude(String packageName) {
            return (includePackages == null || packageName.matches(includePackages)) && (excludePackages == null || !packageName.matches(excludePackages));
        }
    
        @Override
        public boolean hasTransformedResource() {
            return true;
        }
    
        @Override
        public void modifyOutputStream(ZipOutputStream os, boolean preserveFileTimestamps) {
            ZipEntry entry = new ZipEntry(MANIFEST_NAME);
            entry.time = TransformerContext.getEntryTimestamp(preserveFileTimestamps, entry.time);
            os.putNextEntry(entry);
            if (manifest != null) {
                ByteArrayOutputStream manifestContents = new ByteArrayOutputStream();
                manifest.write(manifestContents);
                os.write(manifestContents.toByteArray());
            }
        }
    }