📜 ⬆️ ⬇️

We carry the general functionality of applications and configurations of Gradle into separate modules

Often there is a situation when it is necessary to use the same code in different projects and at the same time maintain its relevance on each of them. In this article, I will show how you can put such code into separate components and use them through dependencies inside build.gradle. In addition to the general code, there will also be considered an example with making the general settings of the build.gradle file migrating from application to application.

If you have a disgust when copying the same code on the projects and the desire to fix it somehow, welcome under cat.

The first thing you need to understand what happens in the end and how to use it. All code that will be used in applications must be divided into components, each of which will be specified in the application dependencies. By components, I mean a separate jar file containing a specific implementation, i.e. we will not build one big jar with all classes, our goal is to put each of the functionalities into a separate component. For example, there are several packages in the application that we would like to reuse, let's say these are classes for logging and some other auxiliary ones.
')
 src /
 Main── main
 │ ├── java
 │ │ └── com
 │ │ └── example
 │ │ ├── logger
 │ │ └── util


Therefore we want to get 2 components, logger and util. In order to be able to get a component in your application, you need to store it in a remote maven repository. I chose for a long time what is better to use for this purpose and eventually settled on the Artifactory. It is easily controlled via the web interface, there is a plugin for Gradle that allows you to easily publish the code and everything works fine out of the box. Customized for this article .
In the end, we will create a separate project for all components, and the logger and util packages will replace a couple of lines in the dependencies block of the build.gradle file.

But that is not all. In addition to copying classes, you often have to copy the configuration from build.gradle. Because of this, there are situations when configs do not change synchronously, where it remains an incorrectly specified minimum version, where additional parameters are added for proguard or lint. To avoid this, part of the build file can also be put into components. This can be done using gradle plugins. Therefore, we will create 2 types of components, plug-ins and packages.

In this article I will not discuss the topic of creating plug-ins for Gradle, if you are not familiar with their work, habrahabr has already had more than one article on this topic, so I see no reason to rewrite it in a new way. In addition, all the code with the implementation of plug-ins is available on github (link at the end).

First of all we will create a project, and transfer the necessary packages to it. Now you need to write a config in order for Gradle to collect separate jar files from us from these packages and publish them in the Artifactory. Then I will bring parts of the config to describe a specific action, the full file can be viewed at the end.

To begin with, add some basic information about the repositories to build.gradle and connect plugins to publish the components.

Basic settings
buildscript { repositories { jcenter() } dependencies { classpath 'org.jfrog.buildinfo:build-info-extractor-gradle:3.1.1' } } allprojects { repositories { jcenter() } } // Artifactory apply plugin: 'com.jfrog.artifactory' //     apply plugin: 'maven-publish' //    ,    apply plugin: 'groovy' //           apply from: 'components.gradle' dependencies { //  Gradle  Gradle API compile gradleApi() } 



All that relates directly to working with components is rendered in the components.gradle file.

All possible components are added to the array, each element of which contains information about the name and version. In principle, if necessary, you can add something else, such as a description, license, etc.

 //  def components = [ [name: 'logger', version:'1.0'], [name: 'util', version:'1.0'] ] 


If you execute the command ./gradlew jar, then in the folder build / libs a jar file with all classes will be created, but since We are interested in individual components, we will create a separate task for each component. The following code is added to understand the process, the resulting file will be different.

Creating components
 //       . //      compileUtil  compileLogger,     jar components.each { component -> task "compile${component.name.capitalize()}" (type: Jar) { version component.version classifier component.classifier baseName component.name from sourceSets.main.output //        //     . // include    exclude,      task.include "com/example/${component.name}/**" } } 



Now, if we execute one of the compileUtil or compileLogger tasks, we’ll get a jar file with a specific package inside. This is useful if you need to update a specific component, but if you need to collect everything at once, you want to avoid entering all these tasks. To do this, we will have to inherit our tasks by our type and create a task to perform them.

Creating components
 //  ,        class ComponentsJar extends Jar { } //       task "compile${component.name.capitalize()}" (type: ComponentsJar) { //       task compileComponents {} //          compileComponents.dependsOn { tasks.withType(ComponentsJar) } 



Now we have the compileComponents task, which depends on all the above created ones, having executed it, we get 2 jar files:
 build / libs /
 Log── logger-1.0.jar
 Util── util-1.0.jar


As for plug-ins, their only difference from simply assembled classes is that they need to additionally indicate the name of the plug-in and the class responsible for its implementation. When applying a plug-in using apply plugin: 'pluginName', Gradle searches inside the jar file with the appropriate name and properties: META-INF / gradle-plugins / pluginName.properties. Inside this file is the class handler.
 implementation-class=com.example.MyPluginClass 


Therefore, for plugins, we will have to additionally pack such a file.

Creating plugins
 //  Gradle //    ,    packagePrefix. //        , // packagePrefix        def plugins = [ [name:'android-signing', version:'1.0', packagePrefix:'signing'], [name:'android-library-publishing', version:'1.0', packagePrefix:'publishing'], [name:'android-base', version:'1.0', packagePrefix:'android'] ] //        plugins.each { component -> task "compile${component.name.capitalize()}" (type: ComponentsJar) { //     jar  'plugin' appendix 'plugin' version component.version classifier component.classifier baseName component.name from sourceSets.main.output def componentPackages = [] //         ,         componentPackages.add("META-INF/gradle-plugins/${component.name}.properties") //  ,  ,           componentPackages.add(component.packagePrefix ? "com/example/${component.packagePrefix}/**" : "com/example/${component.name}/**") include componentPackages } } 



In the previous example, the include section indicated two paths, one of which is “META-INF / gradle-plugins / $ {component.name} .properties”, these files are taken from the resources folder. Therefore, for each plugin you need to create such a file.
 src / main /
 Resources── resources
     MET── META-INF
         Dle── gradle-plugins
             And── android-base.properties
             And── android-library-publishing.properties
             And── android-signing.properties 


The contents of android-base.properties looks like this:
 implementation-class=com.example.android.BaseAndroidConfiguration 


Given that the Gradle configuration is written in Groovy, it is logical that the plugins will be written in Groovy. In principle, they can also be written in java, but you don't need it. Therefore, the implementation of plug-ins is located in the appropriate groovy folder:
 src / main /
 Gro── groovy
  Com── com
  Example── example
  And── android
  │ └── BaseAndroidConfiguration.groovy
  Publishing── publishing
  │ └── LibraryPublishingPlugin.groovy
  Signing── signing
  Sign── SigningPlugin.groovy


In my example, I made 3 settings. The basic one, in which the standard android build settings are specified, the config for publishing libraries in Artifactory and the application signature setting.

Actually, now our task compileComponents creates all the necessary files:
 build / libs /
 And── android-base-plugin-1.0.jar
 And── android-library-publishing-plugin-1.0.jar
 And── android-signing-plugin-1.0.jar
 Log── logger-1.0.jar
 Util── util-1.0.jar


Now these jar-nicks need to be published in our repository.
The first step is to prepare the artifacts for maven. Again, to be able to work with one component, we will create separate publications for maven.

Publish Settings
 // ,     class Artifact { String path, groupId, version, id, name } //  Artifact,       //    compileComponents (.  ) def artifacts = []; //  Artifact,      , //     def activeArtifacts = []; //       //   ,           //   artifacts publishing { publications { artifacts.each { art -> "$art.name"(MavenPublication) { groupId art.groupId version = art.version artifactId art.id artifact(art.path) } } } } // artifactory     ,    activeArtifacts //     .        . artifactoryPublish { doFirst { activeArtifacts.each { artifact -> publications(artifact.name) } } } //..          //  ,       artifactory { contextUrl = ArtifactoryUrl publish { repository { // ,      repoKey = 'libs-release-local' username = ArtifactoryUser password = ArtifactoryPassword } } } 



In principle, everything is clear from the comments, for Artifactory we do not specify all publications at once (as is usually done in the examples of working with Artifactory), but fill them out of necessity. This allows you to run compileUtil artifactoryPublish tasks and compile and publish only one component - util. The only thing left is to generate pom.xml for each of the components, but this is all simple. Maven plugin creates a separate task for each publication, with the name generatePomFileForNAMEPublication, and since we created publications by component name, respectively, generatePomFileForUtilPublication tasks are created, and so on.
Now that all the details are described, we will collect everything in a heap.

Summary file components.gradle
 //   ,        class ComponentsJar extends Jar { } // ,     class Artifact { String path, groupId, version, id, name } //  Artifact,       def artifacts = []; //  Artifact,      , //     def activeArtifacts = []; //  def components = [ [name: 'logger', version:'1.0'], [name: 'util', version:'1.0'] ] //  Gradle def plugins = [ [name:'android-signing', version:'1.0', packagePrefix:'signing'], [name:'android-library-publishing', version:'1.0', packagePrefix:'publishing'], [name:'android-base', version:'1.0', packagePrefix:'android'] ] //       def baseTask = { task, component, packages -> task.version component.version task.classifier component.classifier task.baseName component.name task.from sourceSets.main.output def componentPackages = [] //  ,  ,           componentPackages.add(component.packagePrefix ? "com/example/${component.packagePrefix}/**" : "com/example/${component.name}/**") if (packages != null) { componentPackages.addAll(packages) } task.include componentPackages //         def art = new Artifact( name: component.name, groupId: "com.example", path: "$buildDir/libs/$task.archiveName", id: task.appendix == null ? component.name : "$component.name-$task.appendix", version: component.version ) //      artifacts.add(art) //       ,      task.doFirst { //        activeArtifacts.add(art) } //       pom.xml    task.doLast { tasks."generatePomFileFor${art.name.capitalize()}Publication".execute() } } //        components.each { component -> task "compile${component.name.capitalize()}" (type: ComponentsJar) { baseTask(it, component, component.package) } } //        plugins.each { component -> task "compile${component.name.capitalize()}" (type: ComponentsJar) { //     jar   'plugin' (        ) appendix 'plugin' //         ,         def packages = ["META-INF/gradle-plugins/${component.name}.properties"] if (component.package != null) { packages.addAll(component.package) } baseTask(it, component, packages); } } //       task compileComponents {} //          compileComponents.dependsOn { tasks.withType(ComponentsJar) } jar { //    jar     doLast { new File(it.destinationDir, "${project.name}.jar").delete() } } //       //   ,           //    artifacts publishing { publications { artifacts.each { art -> "$art.name"(MavenPublication) { groupId art.groupId version = art.version artifactId art.id artifact(art.path) } } } } // artifactory     ,        artifactoryPublish { doFirst { activeArtifacts.each { artifact -> publications(artifact.name) } } } artifactory { contextUrl = ArtifactoryUrl publish { repository { // ,      repoKey = 'libs-release-local' username = ArtifactoryUser password = ArtifactoryPassword } } } 



The cofishing uses additional variable ArtifactoryUser, ArtifactoryPassword and ArtifactoryUrl. I specifically took them out of the project. This will allow you to easily manage these parameters in different environments. I added them globally to the ~ / .gradle / gradle.properties file:
 ArtifactoryUrl=http://localhost:8081 ArtifactoryUser=admin ArtifactoryPassword=password 


Actually that's all. Assembled components can now be specified in dependencies, after which the project can safely refer to the necessary classes.

 dependencie { compile 'com.example:util:1.0' compile 'com.example:logger:1.0' } 


In order for Gradle to find the components, you must specify our repository.
 allprojects { repositories { jcenter() maven { url "$ArtifactoryUrl/libs-release-local" credentials { username = ArtifactoryUser password = ArtifactoryPassword } } } } 


The plug-ins are connected in two lines, one is indicated in the buildscript dependencies, the second is directly when using the plug-in, now you can compare what build.gradle was and how it became

It was
 buildscript { repositories { jcenter() } dependencies { classpath 'com.android.tools.build:gradle:1.3.0' } } allprojects { repositories { jcenter() } } apply plugin: 'com.android.application' android { compileSdkVersion 22 buildToolsVersion "22.0.1" publishNonDefault true defaultConfig { minSdkVersion 15 targetSdkVersion 21 } packagingOptions { exclude 'META-INF/NOTICE.txt' exclude 'META-INF/LICENSE.txt' } testOptions { unitTests.returnDefaultValues = true } lintOptions { abortOnError false } if(project.hasProperty("debugSigningPropertiesPath") && project.hasProperty("releaseSigningPropertiesPath")) { File debugPropsFile = new File(System.getenv('HOME') + "/" + project.property("debugSigningPropertiesPath")) File releasePropsFile = new File(System.getenv('HOME') + "/" + project.property("releaseSigningPropertiesPath")) if(debugPropsFile.exists() && releasePropsFile.exists()) { Properties debugProps = new Properties() debugProps.load(new FileInputStream(debugPropsFile)) Properties releaseProps = new Properties() releaseProps.load(new FileInputStream(releasePropsFile)) signingConfigs { debug { storeFile file(debugPropsFile.getParent() + "/" + debugProps['keystore']) storePassword debugProps['keystore.password'] keyAlias debugProps['keyAlias'] keyPassword debugProps['keyPassword'] } release { storeFile file(releasePropsFile.getParent() + "/" + releaseProps['keystore']) storePassword releaseProps['keystore.password'] keyAlias releaseProps['keyAlias'] keyPassword releaseProps['keyPassword'] } } buildTypes { debug { signingConfig signingConfigs.debug } release { signingConfig signingConfigs.release } } } } } 

It became
 buildscript { repositories { jcenter() maven { url "$ArtifactoryUrl/libs-release-local" credentials { username = ArtifactoryUser password = ArtifactoryPassword } } } dependencies { classpath 'com.android.tools.build:gradle:1.3.0' classpath 'org.jfrog.buildinfo:build-info-extractor-gradle:3.1.1' classpath 'com.example:android-library-publishing-plugin:1.0' classpath 'com.example:android-signing-plugin:1.0' classpath 'com.example:android-base-plugin:1.0' } } allprojects { repositories { jcenter() maven { url "$ArtifactoryUrl/libs-release-local" credentials { username = ArtifactoryUser password = ArtifactoryPassword } } } } apply plugin: 'com.android.application' apply plugin: 'android-library-publishing' apply plugin: 'android-signing' apply plugin: 'android-base' 


I posted the whole project on github .
That's all, constructive criticism is always welcome.

Source: https://habr.com/ru/post/267311/


All Articles