January 13, 2017

A Gradle Build Receipt (Part 2)

Picking up where I left off with my A Gradle Build Receipt (Part 1) post, I now extend the BuildReceiptTask to include manifest (related) attributes.

Cutting to the chase once more, here’s a snippet of the build receipt which shows the manifest attributes associated with the component being built…

{
    "dependencies": [
        .
        .
        .
    ],
    "manifest": {
        "artifactGroup": "com.example.rup.system.topology",
        "artifactName": "topology-rest",
        "artifactVersion": "4.2.10",
        "buildTimestamp": "2017-01-09 07:48:49 EST",
        "buildJdk": "1.8.0_60 (Oracle Corporation 25.60-b23)",
        "buildTool": "Gradle 2.5-rup-01",
        "builtBy": "tmuldoon",
        "builtFromProject", "system-topology",
        "implementationVersion": "4.2.10",
        "implementationTitle": "topology-rest"
    }
}

Herein lies the completed implementation of the BuildReceiptTask…

package com.example.scm.gradle.plugins.task
 
import java.util.List;
import java.util.Map;
import java.util.Set;
 
import groovy.json.JsonBuilder
import groovy.json.JsonOutput
 
import org.gradle.api.DefaultTask
import org.gradle.api.Project
import org.gradle.api.artifacts.component.ComponentIdentifier
import org.gradle.api.artifacts.component.ComponentSelector
import org.gradle.api.artifacts.component.ModuleComponentIdentifier
import org.gradle.api.artifacts.component.ModuleComponentSelector
import org.gradle.api.artifacts.result.DependencyResult
import org.gradle.api.artifacts.result.ResolutionResult
import org.gradle.api.artifacts.result.ResolvedDependencyResult
import org.gradle.api.logging.Logger
import org.gradle.api.logging.Logging
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.TaskAction
import org.gradle.internal.jvm.Jvm
import org.gradle.util.GradleVersion
 
/**
 * Captures dependencies for the project being built.
 */
class BuildReceiptTask extends DefaultTask {
    /**
     * Adds the buildReceipt task.
     *
     * @param project the gradle project.
     */
    public static void addBuildReceiptTask(Project project) {
        project.task('buildReceipt', type: BuildReceiptTask) {
            group 'Build'
            description 'Captures manifest attributes, dependencies, etc.'
        }
       
        project.artifacts {
            archives project.tasks.buildReceipt.outputFile
        }
 
        project.assemble.dependsOn project.buildReceipt
        project.uploadArchives.dependsOn project.buildReceipt
    }
 
    /**
     * The output file.
     */
    @OutputFile
    def File outputFile
   
    /**
     * Default constructor.
     */
    public BuildReceiptTask() {
        filename = "${project.buildDir}/build-receipt/build-receipt.json"
        outputFile = new File(filename)
    }
 
    /**
     * The gradle task action (invoked when the buildReceipt task is 
     * executed).
     */
    @TaskAction
    void performTask() {
        msg = "[BuildReceiptTask] Create a build receipt including manifest attributes, dependencies, etc."
        project.logger.info msg
 
        Map<String, String> manifest = getManifest()
        List<Dependency> dependencies = getDependencies(project)
        BuildReceipt buildReceipt = new BuildReceipt(manifest, dependencies)
 
        writeBuildReceipt(buildReceipt)
    }
 
    /**
     * Captures manifest attributes.
     *
     * @return manifest attributes (in property-name case).
     */
    Map<String, String> getManifest() {
        Map<String, String> manifest = new HashMap<String, String>()
        manifest.put("artifactGroup", project.group)
        manifest.put("artifactName", project.name)
        manifest.put("artifactVersion", project.version)
        manifest.put("buildTimestamp", getTimestamp())
        manifest.put("buildJdk", Jvm.current().toString())
        manifest.put("buildTool", GradleVersion.current().toString())
        manifest.put("builtBy", System.getProperty('BUILD_USER_ID', System.getProperty('user.name')))
        manifest.put("builtFromProject", project.rootProject.name)
 
        // These are redundant with our custom attributes, but keep them 
        // since they are fairly standard in the Java ecosystem
        manifest.put("implementationTitle", project.name)
        manifest.put("implementationVersion", project.version)
 
        return manifest
    }
 
    /**
     * Returns the current date and time.
     *
     * @return the current date and time (as a String).
     */
    String getTimestamp() {
        def cal = Calendar.instance
        def df = new java.text.SimpleDateFormat('yyyy-MM-dd HH:mm:ss z', Locale.ENGLISH)
        df.format(cal.time)
    }
 
    /**
     * Captures dependencies (for each and every configuration).
     *
     * @param project gradle project
     */
    List<Dependency> getDependencies(Project project) {
        List<Dependency> dependencies = new ArrayList<Dependency>();
       
        project.configurations.each { conf ->
            ResolutionResult rr = conf.incoming.resolutionResult
            Set<DependencyResult> dependencyResults = rr.getAllDependencies().findAll()
            if (dependencyResults.size() > 0) {
                Map<ModuleComponentIdentifier, Set<ModuleComponentSelector>> dependencyMap =
                        getDependencyMap(dependencyResults)
   
                dependencyMap.each{ componentIdentifier, componentSelectorSet ->
                    String group = componentIdentifier.getGroup()
                    String module = componentIdentifier.getModule()
                    String version = componentIdentifier.getVersion()
                   
                    List<String> requestedVersions = new ArrayList<String>()
                    componentSelectorSet.each{ componentSelector ->
                        requestedVersions.add(componentSelector.getVersion())
                    }
                    Dependency dependency = new Dependency(requestedVersions, conf.name, group, module, version)
                    dependencies.add(dependency)
                }
            }
        }
       
        return dependencies
    }
 
    /**
     * Returns a map containing each and every "selected" dependency and their associated "requested" components.
     *
     * @param dependencyResults the dependency results (per gradle)
     * @return dependency map
     */
    Map<ModuleComponentIdentifier, Set<ModuleComponentSelector>> getDependencyMap(Set<DependencyResult> dependencyResults) {
        Map<ModuleComponentIdentifier, Set<ModuleComponentSelector>> dependencyMap =
                new HashMap<ModuleComponentIdentifier, Set<ModuleComponentSelector>>();
 
        dependencyResults.each { dr ->
            ResolvedDependencyResult rdr = (ResolvedDependencyResult) dr
           
            ComponentIdentifier componentIdentifier = rdr.selected.getId()
            project.logger.debug("[BuildReceiptTask] Selected dependency... " + componentIdentifier.toString());
            if (componentIdentifier instanceof ModuleComponentIdentifier) {
                ComponentSelector componentSelector = rdr.requested
               
                // Note, the component selector set holds the "requested" components.
                Set<ComponentSelector> componentSelectorSet = dependencyMap.get(componentIdentifier);
                if (componentSelectorSet == null) {
                    componentSelectorSet = new HashSet<ComponentSelector>();
                }
               
                componentSelectorSet.add(componentSelector)
                dependencyMap.put(componentIdentifier, componentSelectorSet)
            }
        }
       
        return dependencyMap;
    }
 
    /**
     * Writes the build receipt to the task's output file.
     */
    void writeBuildReceipt(BuildReceipt buildReceipt) {
        String jsonOutput = JsonOutput.toJson(buildReceipt)
        String formattedJsonOutput = JsonOutput.prettyPrint(jsonOutput)
        outputFile.withWriter { out ->
            out.write(formattedJsonOutput)
        }
    }
   
    class BuildReceipt {
        Map<String, String> manifest
        List<Dependency> dependencies
       
        BuildReceipt(Map<String, String> manifest, List<Dependency> dependencies) {
            this.manifest = manifest
            this.dependencies = dependencies
        }
    }
 
    class Dependency {
        List<String> requestedVersions
        String configuration
        String group
        String name
        String version
        
        Dependency(List<String> requestedVersions, String configuration,
                 String group, String name, String version) {
            this.requestedVersions = requestedVersions
            this.configuration = configuration
            this.group = group
            this.name = name
            this.version = version
        }
    }
}

Tom Muldoon

Software Architect