The Lazy Developer’s Way to an Up-To-Date Libraries List
Last time I shared some tips on how to use libraries well. I now want to delve deeper into one of those: Know What Libraries You Use. Last week I set out to create such a list of embedded components for our product. This is a requirement for our Security Development Lifecycle (SDL). However, it’s not a fun task. As a developer, I want to write code, not update documents! So I turned to my friends Gradle and Groovy, with a little help from Jenkins and Confluence.
Gradle Dependencies
We use Gradle to build our product, and Gradle maintains the dependencies we have on third-party components.
Our build defines a list of names of configurations for embedded components, copyBundleConfigurations
, for copying those to the distribution directory. From there, I get to the external dependencies using Groovy’s collection methods:
def externalDependencies() { copyBundleConfigurations.collectMany { configurations[it].allDependencies }.findAll { !(it instanceof ProjectDependency) && it.group && !it.group.startsWith('com.emc') } }
Adding Required Information
However, Gradle dependencies don’t contain all the required information. For instance, we need the license under which the library is distributed, so that we can ask the Legal department permission for using it. So I added a simple XML file to hold the additional info. Combining that information with the dependencies that Gradle maintains is easy using Groovy’s XML support:
ext.embeddedComponentsInfo = 'embeddedComponents.xml' def externalDependencyInfos() { def result = new TreeMap() def componentInfo = new XmlSlurper() .parse(embeddedComponentsInfo) externalDependencies().each { dependency -> def info = componentInfo.component.find { it.id == '$dependency.group:$dependency.name' && it.friendlyName?.text() } if (!info.isEmpty()) { def component = [ 'id': info.id, 'friendlyName': info.friendlyName.text(), 'version': dependency.version, 'latestVersion': info.latestVersion.text(), 'license': info.license.text(), 'licenseUrl': info.licenseUrl.text(), 'comment': info.comment.text() ] result.put component.friendlyName, component } } result.values() }
I then created a Gradle task to write the information to an HTML file. Our Jenkins build executes this task, so that we always have an up-to-date list. I used Confluence’s html-include
macro to include the HTML file in our Wiki. Now our Wiki is always up-to-date.
Automatically Looking Up Missing Information
The next problem was to populate the XML file with additional information. Had we had this file from the start, adding that information manually would not have been a big deal. In our case, we already had over a hundred dependencies, so automation was in order. First I identified the components that miss the required information:
def missingExternalDependencies() { def componentInfo = new XmlSlurper() .parse(embeddedComponentsInfo) externalDependencies().findAll { dependency -> componentInfo.component.find { it.id == '$dependency.group:$dependency.name' && it.friendlyName?.text() }.isEmpty() }.collect { '$it.group:$it.name' }.sort() }
Next, I wanted to automatically look up the missing information and add it to the XML file (using Groovy’s MarkupBuilder
). In case the required information can’t be found, the build should fail:
project.afterEvaluate { def missingComponents = missingExternalDependencies() if (!missingComponents.isEmpty()) { def manualComponents = [] def writer = new StringWriter() def xml = new MarkupBuilder(writer) xml.expandEmptyElements = true println 'Looking up information on new dependencies:' xml.components { externalDependencyInfos().each { existingComponent -> component { id(existingComponent.id) friendlyName(existingComponent.friendlyName) latestVersion(existingComponent.latestVersion) license(existingComponent.license) licenseUrl(existingComponent.licenseUrl) approved(existingComponent.approved) comment(existingComponent.comment) } } missingComponents.each { missingComponent -> def lookedUpComponent = collectInfo(missingComponent) component { id(missingComponent) friendlyName(lookedUpComponent.friendlyName) latestVersion(lookedUpComponent.latestVersion) license(lookedUpComponent.license) licenseUrl(lookedUpComponent.licenseUrl) approved('?') comment(lookedUpComponent.comment) } if (!lookedUpComponent.friendlyName || !lookedUpComponent.latestVersion || !lookedUpComponent.license) { manualComponents.add lookedUpComponent.id println ' => Please enter information manually' } } } writer.close() def embeddedComponentsFile = project.file(embeddedComponentsInfo) embeddedComponentsFile.text = writer.toString() if (!manualComponents.isEmpty()) { throw new GradleException('Missing library information') } } }
Anyone who adds a dependency in the future is now forced to add the required information. So all that is left to implement is the collectInfo()
method. There are two primary sources that I used to look up the required information: the SpringSource Enterprise Bundle Repository holds OSGi bundle versions of common libraries, while Maven Central holds regular jars.
Extracting information from those sources is a matter of downloading and parsing XML and HTML files. This is easy enough with Groovy’s String.toURL()
and URL.eachLine()
methods and support for regular expressions.
Conclusion
All of this took me a couple of days to build, but I feel that the investment is well worth it, since I no longer have to worry about the list of used libraries being out of date.
Reference: The Lazy Developer’s Way to an Up-To-Date Libraries List from our JCG partner Remon Sinnema at the Secure Software Development blog.