Skip to content

Library signatures

Vyacheslav Rusakov edited this page Feb 17, 2021 · 5 revisions

Animalsniffer could be used to check compatibility with some 3rd party library.

Example

The example is very synthetic but should clearly show how to use signatures for checking compatibility with libraries.

Complete signature

Suppose you're using in your project slf4j-api 1.7.25 and want to be sure that you're also compatible with 1.5.3. We can generate a signature for slf4j 1.5.3 and use it to validate compatibility.

plugins {
    id 'java'
    id 'ru.vyarus.animalsniffer'
}

// use custom configuration to build signature            
configurations.create('newsig')
            
// build slf4j signature as an extension to jdk signature
task sig(type: ru.vyarus.gradle.plugin.animalsniffer.signature.BuildSignatureTask) {
    signatures configurations.signature
    files configurations.newsig
}

animalsniffer {
    // use generated signature instead of configuration
    signatures = sig.outputFiles
    excludeJars 'slf4j-*'
}                        

repositories { mavenCentral()}
dependencies {
    // this signature is used only to build custom signature, but not in check directly
    signature 'org.codehaus.mojo.signature:java16-sun:1.0@signature'
    // dependency that must to be excluded to be able to check with newly generated signature
    implementation 'org.slf4j:slf4j-api:1.7.25'
                
    // configuration used only to build signature
    newsig 'org.slf4j:slf4j-api:1.5.3'
}

Here we use custom configuration to get old slf4j version and build jdk+old slf4j signature with the custom task. Custom signature is used for check.

NOTE that it is important to exclude library jar, otherwise signature will not detect anything (real jar will mark all usages as valid).

If we run check on class:

public class Sample {
    public static void main(String[] args) {
        // api present in 1.5.3 (line must not be errored)
        LoggerFactory.getLogger("goodapi");
        // method appear in 1.5.4
        MarkerFactory.getMarker("sample").hasReferences();
    }
}

It will detect api change:

[Undefined reference] custsig.(Sample.java:12)
  >> boolean org.slf4j.Marker.hasReferences()

Partial signature

Most likely, small library signature would be built without jdk (library signature only). In such case, we would need to merge our required jdk signature with small library signatire (it is required because if use just small signature, all jdk classes usages will be treated as violations).

Note that you may base on android signature or use library signature in both android and java projects. This makes small signature re-usable.

Cache task is not merging signatures by default because commonly complete signatures are used (jdk, android) and merge is not desired. But it can merge signatures. Modified first example:

plugins {
    id 'java'
    id 'ru.vyarus.animalsniffer'
}

// creating signature just for slf4j library
configurations.create('newsig')
task sig(type: ru.vyarus.gradle.plugin.animalsniffer.signature.BuildSignatureTask) {
    files configurations.newsig
}

animalsniffer {
    // using both signatures for check
    signatures = files(configurations.signature, sig.outputFiles)
    excludeJars 'slf4j-*'
    cache {
        enabled = true

        // cache would merge signatures into single signature
        mergeSignatures = true
    }
}                        

repositories { mavenCentral()}
dependencies {
    signature 'org.codehaus.mojo.signature:java16-sun:1.0@signature'
    // dependency that must to be excluded to be able to check with newly generated signatire
    implementation 'org.slf4j:slf4j-api:1.7.25'
    
    // configuration used only to build signature
    newsig 'org.slf4j:slf4j-api:1.5.3'
}

If library signature is published into maven repository, the configuration will become:

plugins {
    id 'java'
    id 'ru.vyarus.animalsniffer'
}

animalsniffer {
    excludeJars 'slf4j-*'
    cache {
        enabled = true
        mergeSignatures = true
    }
}                        

repositories { mavenCentral()}
dependencies {
    signature 'org.codehaus.mojo.signature:java16-sun:1.0@signature'
    // note this is not real, but just to show the idea - all signatures confiugred in one place
    signature 'org.slf4j:slf4j-api:1.5.3@signature'

    implementation 'org.slf4j:slf4j-api:1.7.25'
}

Partial without cache

In some situations, the cache could not be used.

For example, if you want to check with both jdk and android and need to use library signature. In this case, cache would merge everything, which is not desirable. For such cases, use custom signature tasks to build custom signatures (as in the first example).

Another caveat is possible signature build conflict. In this case custom build tasks would also help.

Configuration

Jar exclude

As shown above, you need to exclude jars, covered with library signatures.

animalsniffer {
    excludeJars 'slf4j-*'
}

Excluding occure on classpath jars. Jar files are matched without extension. Convention for classpath file names is artifactId-version.

For example, slf4j jar would be slf4j-api-1.5.3.jar, but pattern would be matched with slf4j-api-1.5.3.

Patterns are actually regular expressions, but in most cases this is not required and that's why '*' symbol is supported: it is replaced to '.+'. By definition above, slf4j-.+ regexp will be used for matching.

Many patterns could be declared:

excludeJars 'slf4j-*', 'some-*'

Configuration method could be called multiple times (addition):

excludeJars 'slf4j-*', 'some-*'
excludeJars 'other-*'

Property may be configured directly, but it would override previous values:

excludeJars = ['slf4j-*', 'some-*']

Signatures

Use project.files() to group multiple file sets (or specify separate files, url etc).

animalsniffer {
    signatures = files(configurations.signature, sig.outputFiles)
}

Note that working with FileCollection allows evaluating files lazily. In the example above, sig.outputFiles files do not exist in the configuration time, but as it's a lazy collection, files being resolved correctly. Moreover using this method puts implicit depdendency on task. project.files() preserve laziness when merging multiple collections.

NOTE: do not rely on task outputs, because build task declares only output directory and task.outputs.file will contain ONLY directory itself. Special task.outputFiles method should be used.