Dependency management team
Publishing basics
Customizing publications
The Gradle component model
Published metadata
Versioning
The plugins: maven-publish
and ivy-publish
The concept of a publication
One or more target repositories
This presentation does not cover the legacy publication system, deprecated in Gradle 6.0 |
plugins {
`java-library`
`maven-publish`
}
group = "org.test"
version = "0.0.1"
publishing {
publications {
create<MavenPublication>("main") {
from(components["java"])
}
}
}
publishing {
repositories {
maven {
name = "artifactory"
url = uri("http://localhost:8081/artifactory/example-repo-local")
}
}
}
Multiple repositories can be defined that way, in case you need to publish to more than one location.
maven {
name = "testRepo"
url = uri("$buildDir/repo")
}
This test repository can be useful to review the produced publication files, before uploading them to their final destination.
The publication plugins will define a number of tasks based on the declared publications and repositories:
publish<Publication>PublicationTo<Repository>Repository
to publish a specific publication to a specific repository
publishAllPublicationsTo<Repository>Repository
to publish all publications to a specific repository
publish
to publish all publications to all repositories
publishToMavenLocal
to publish all maven publications to the Maven local repository
This is not executed when using publish
generate<MetadataType>FileFor<Publication>Publication
to generate the metadata file (Ivy, Maven or Gradle) for a specific publication.
Requires the signing
plugin
signing {
setRequired({ !version.toString().endsWith("-SNAPSHOT") })
useGpgCmd()
sign(publishing.publications["main"])
}
The artifacts of the selected component
Usually the JAR file for a Java project
The metadata of the selected component
Maven or Ivy depending on the repository type
Gradle Module Metadata by default starting with Gradle 6.0
Extra files
Checksums: MD5, SHA1, SHA256, SHA512 (last two with Gradle 6.0)
Signatures if signing configured
Publication of javadoc and sources JARs
Your project defines additional archives
Test fixtures
Optional features
In all the cases above, we want to publish not only the added artifact but also its relevant metadata.
java {
withJavadocJar()
withSourcesJar()
}
Produces and attaches the JAR to the publication …
As variants, with the right variant attributes
plugins {
`java-test-fixtures`
}
dependencies {
testFixturesApi("org.assertj:assertj-core:3.13.2")
}
Gives you an extra source set: testFixtures
Gives you configurations to declare dependencies: testFixturesApi
and testFixturesImplementation
Creates the necessary configurations for compiling, running and consuming the test fixtures
All wired for you. Nothing to do.
Details in later sections
Additional features of your library, that may require:
Additional dependencies
Additional artifact
while still using a single project for development
Test fixtures are one use case for this concept
java {
registerFeature("optionalFeature") {
usingSourceSet(sourceSets["main"])
}
}
Source set choice indicates whether optional aspect is bundled with the main JAR or as an additional artifact
Creates the configurations for dependency declarations and wires them properly
All wired for you. Nothing to do.
Details in later sections
implementation("org.test:publish-feature:0.0.1") {
capabilities {
requireCapability("org.test:publish-feature-optional-feature")
}
}
Selection happens through the use of capabilities
java {
registerFeature("mysqlSupport") {
usingSourceSet(sourceSets["main"])
capability("org.gradle.demo", "producer-db-support", "1.0")
capability("org.gradle.demo", "producer-mysql-support", "1.0")
}
registerFeature("postgresSupport") {
usingSourceSet(sourceSets["main"])
capability("org.gradle.demo", "producer-db-support", "1.0")
capability("org.gradle.demo", "producer-postgres-support", "1.0")
}
}
Since only one module can provide a given capability, sharing a capability forces consumer to chose only one capability provider.
from(components["java"])
Creates safe boundaries between projects
Ground for the publication model
Artifacts are attached to configurations
which are exposed as variants
which belong to a component
“How do I share the output of a task between subprojects?”
Classic (but wrong) answer:
project(":subproject").tasks.getByName("someTask")
Unsafe
Poorly modeled
Producer: create an outgoing configuration
val instrumentedJars by configurations.creating {
isCanBeConsumed = true
isCanBeResolved = false
// If you want this configuration to share the same dependencies,
// otherwise omit this line
extendsFrom(
configurations["implementation"],
configurations["runtimeOnly"])
}
Producer: attach the artifact to the outgoing configuration
val instrumentedJar = tasks.register<Jar>("instrumentedJar") {
archiveClassifier.set("instrumented")
}
instrumentedJars.outgoing.artifact(instrumentedJar)
Consumer: declare a resolvable configuration
val instrumentedClasspath by configurations.creating {
isCanBeConsumed = false
isCanBeResolved = true
}
Consumer: declaring a dependency on the configuration
dependencies {
instrumentedClasspath(project(mapOf(
"path" to ":producer",
"configuration" to "instrumentedJars")))
}
Let’s improve modeling on the producer side
Producer:
plugins {
`java-library`
`instrumented-jars`
}
Moved logic to buildSrc
Declaration of attributes
private fun Project.createOutgoingConfiguration(): Configuration {
val instrumentedJars by configurations.creating {
isCanBeConsumed = true
isCanBeResolved = false
attributes {
attribute(Category.CATEGORY_ATTRIBUTE, namedAttribute(Category.LIBRARY))
attribute(Usage.USAGE_ATTRIBUTE, namedAttribute(Usage.JAVA_RUNTIME))
attribute(Bundling.BUNDLING_ATTRIBUTE, namedAttribute(Bundling.EXTERNAL))
attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, JavaVersion.current().majorVersion.toInt())
attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, namedAttribute("instrumented-jar"))
}
}
return instrumentedJars
}
Consumer: may now use regular dependency declarations
dependencies {
implementation(project(":producer"))
testImplementation("junit:junit:4.12")
}
Consumer: asks for a different variant for test runtime
configurations {
testRuntimeClasspath {
attributes {
attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE,
objects.named(LibraryElements::class.java, "instrumented-jar"))
}
}
}
Consumer: now compile classpath and test runtime classpath use different variants
> Task :consumer:resolveInstrumentedClasses
[/producer/build/classes/java/main, junit-4.12.jar, hamcrest-core-1.3.jar]
[producer-instrumented.jar, junit-4.12.jar, hamcrest-core-1.3.jar]
Producer can add variants to existing components
val javaComponent = components.findByName("java") as AdhocComponentWithVariants
javaComponent.addVariantsFromConfiguration(outgoing) {
// dependencies for this variant are considered runtime dependencies
mapToMavenScope("runtime")
// and also optional dependencies, because we don't want them to leak
mapToOptional()
}
Or producer can create a full adhoc component
// create an adhoc component
val adhocComponent = softwareComponentFactory.adhoc("myAdhocComponent")
// add it to the list of components that this project declares
components.add(adhocComponent)
// and register a variant for publication
adhocComponent.addVariantsFromConfiguration(outgoing) {
mapToMavenScope("runtime")
}
Sometimes you don’t need to publish all variants for external consumption
Example: test fixtures can be used between projects but shouldn’t be available from external contributors
val javaComponent = components["java"] as AdhocComponentWithVariants
javaComponent.withVariantsFromConfiguration(
configurations.testFixturesApiElements) {
skip()
}
Artifactory plugin
Leverages the maven-plugin
Adds "build information" when uploading to Artifactory
plugins {
id("com.jfrog.artifactory") version "4.10.0"
}
No support for Gradle Module Metadata yet! |
Bintray plugin
Leverages the artifactory plugin
Provides way to publish to JCenter
Provides way to synchronize to Maven Central (optional)
plugins {
id("com.jfrog.bintray") version "1.8.4"
}
No support for Gradle Module Metadata yet! |
Apache Maven™ POM file
Only in Maven repositories
Apache Ivy™ ivy.xml file
Only in Ivy repositories
Gradle Module Metadata file
In both types of repositories in addition to one of the above
Used by Gradle 6 if available instead of the above
Carries information on variants, dependency constraints, custom artifacts
junit-jupiter-api/
├── junit-jupiter-api-5.6.0.pom ('published-with-gradle-metadata' marker)
├── junit-jupiter-api-5.6.0.module
├── junit-jupiter-api-5.6.0.jar
├── junit-jupiter-api-5.6.0-javadoc.jar
├── junit-jupiter-api-5.6.0-sources.jar
Information in module
missing in pom
includes:
Variants with the knowledge about javadoc.jar
and sources.jar
Constraint on other JUnit 5.6.0
modules to align versions
junit-jupiter-api-5.6.0.module (excerpt)
"variants": [
{ "name": "apiElements" ... },
{ "name": "runtimeElements" ... },
{ "name": "javadocElements",
"attributes": {
"org.gradle.category": "documentation",
"org.gradle.docstype": "javadoc",
},
"files": [
{ "url": "junit-jupiter-api-5.6.0-javadoc.jar" ... }
]},
{ "name": "sourcesElements" ... },
build.gradle.kts (consumer example)
val javadoc by configurations.creating {
attributes.attribute(CATEGORY_ATTRIBUTE, objects.named(DOCUMENTATION))
attributes.attribute(DOCS_TYPE_ATTRIBUTE, objects.named(JAVADOC))
}
dependencies { javadoc("org.junit.jupiter:junit-jupiter-api:5.6.0") }
tasks.create("doSomethingWithJavadocs") { doLast { javadoc.files } }
junit-jupiter-api-5.6.0.module (excerpt)
"variants": [
{
"name": "apiElements",
"attributes": { "org.gradle.category": "library", ... },
"dependencies": [
{
"group": "org.junit",
"module": "junit-bom",
"version": { "requires": "5.6.0" },
"attributes": { "org.gradle.category": "platform" },
}
]
},
junit-bom-5.6.0.module (excerpt)
"variants": [
{
"name": "apiElements",
"attributes": { "org.gradle.category": "platform", ... },
"dependencyConstraints": [
{
"group": "org.junit.jupiter",
"module": "junit-jupiter-engine",
"version": { "requires": "5.6.0" }
}
]
},
build.gradle.kts (consumer example)
dependencies {
implementation("org.junit.jupiter:junit-jupiter-api:5.6.0")
// ↑↑↑ brings in 'junit-bom:5.6.0'
implementation("org.junit.jupiter:junit-jupiter-engine:5.5.2")
// ↑↑↑ thanks to 'junit-bom:5.6.0' it will align to 5.6.0
}
Gradle | POM | Ivy | |
---|---|---|---|
Dependency constraints | ✔ | X* | X |
Dependencies on platforms | ✔ | X* | X |
Rich version constraints | ✔ | X | X |
* <dependencyManagement>
blocks are added to poms
Gradle | POM | Ivy | |
---|---|---|---|
Component capabilities | ✔ | X | X |
Feature variants | ✔ | X** | ✔ |
Custom components | ✔ | X** | X |
** Artifacts have to be addressed directly by consumer (e.g. by classifier)
guava-28.1.module (how could it look like?)
"variants": [
{ "name": "jdk6ApiElements", "dependencies": [ ... ],
"attributes": { "org.gradle.jvm.version": 6, ... },
"files": [{ "url": "../28.1-android/guava-28.1-android.jar" }]
},
{ "name": "jdk8ApiElements", "dependencies": [ ... ],
"attributes": { "org.gradle.jvm.version": 8, ... },
"files": [{ "url": "../28.1-jre/guava-28.1-jre.jar" }]
},
{ "name": "jdk6RuntimeElements" ... },
{ "name": "jdk8RuntimeElements" ... },
build.gradle.kts (consumer example)
dependencies {
implementation("com.google.guava:guava:28.1-jre")
}
java {
// 6 or 7 gets 'android.jar', 8+ gets 'jre.jar', <6 fails
targetCompatibility = JavaVersion.VERSION_1_6
}
Your build file declares a number of versions
e.g., commons-lang3:3.3
Not necessarily the resolved one:
e.g., commons-lang3:3.3 → 3.4 (conflict resolution)
What about dynamic dependencies?
e.g. commons-lang3:3.+
Gradle lets you choose between two modes: declared or resolved versions
defaults to the declared versions
Pros
Effectively what you need
Maximum expressiveness thanks to rich versions
Cons
Requires diligence in what is declared
Result may be very different
Pros
What you resolve is what you get
Stable resolution for consumers (no dynamic)
Cons
Lossy mapping (ranges disappear, for example)
Maybe unintentional
Always approximate (what resolution to use?)
Maybe incorrect (e.g. substitutions)
Makes the mapping lossy for Gradle as well
flattens to require
or strictly
if the declared version is strictly, the resolved is as well
publishing {
publications {
create<MavenPublication>("mavenJava") {
versionMapping {
usage("java-api") {
fromResolutionOf("runtimeClasspath")
}
usage("java-runtime") {
fromResolutionResult()
}
}
}
}
}
Locking guarantees same resolution result
But lock files are unknown to consumers
Using locking + resolved result makes sense
More strategies
still publish ranges, prefer the resolved version
fail if resolved version disagrees with declared version
Version is simply a project
property
Open as to where to publish
-SNAPSHOT
, based on the Maven convention
Changing aspect is a problem for reproducibility
Unique releases
Keeping up with releases requires work / automation
Maven local repository
Convenient but not without issues
Consider using repository content filtering to limit its impact
Repository (local or remote)
Requires more infrastructure
Live with the -SNAPSHOT
limitations
Use dynamic versions and locking for easier automated updates
Add publication of resolved versions
During development, integration can use:
Publication and releases
Composite builds
Source dependencies
nebula-release-plugin
supports multiple release types
Integrates with Git
final, candidate, snapshots, unique snapshots, …
Give it a try!
This is really where all these concepts come into play inside an organisation.
Stay tuned for a future webinar on this, and how Gradle can help!
Switch to the maven-publish
or ivy-plugin
Supports publishing POM/Ivy + Gradle Module Metadata
Gradle Module Metadata → richest
Flexible support for custom components
already used by 3rd party plugins (Kotlin Native, Android, …)