In many projects I've worked I've come to appreciate the advantages that using a continuous integration tool like TeamCity. During the development of last Android application, available on Google Play at OCL Lab Results, I saw the need to include or exclude certain files in the assets of the same.
I decided to go into the benefits offered by a build automation system like Gradle. Let me make one thing clear before starting the details, this article is not a tutorial on Gradle, so I hope you're familiar with it before reading, although I confess that it is not very difficult to understand its main components. All code shown below is designed with Gradle 2.1 and Android Studio.
First you need to ensure the existence of the folder of assets to be included in the apk. So we will create the next task to verify the existence of that folder:
We are now ready to generate our .apk file with the assets already included, so we write the following task:
This is not enough, since the tasks of which depends assembleArtifact be executed before the same but in any order. Therefore, it is possible that the assembleRelease task runs without is executed before the copyAssetFiles task, so we need to specify the order of precedence of them.
We are ready to configure our TeamCity. On Gradle step options, we have a Gradle Parameters section, you need specify on Gradle tasks option a task to invoke, in this case:
So let's parse this file with XmlPullParser to obtain the variable that we need. Let's take our code to another level, using one of the benefits of Groovy, which allows us to define our tasks as POGOs, YESS!!!!, as you readed, Plain Old Groovy Objects. When we write a code must try to make it more flexible and reusable as possible. Therefore we will implement a method to extract the value of an attribute will in an xml file.
You can set the task to find the path of assets folder as:
The expression $project.name return the name of your project. If we want the path of AndroidManifest.xml file, only have to declare the task as follow:
Now we are ready, in the Gradle Settings of your Continuous Integration, invoke one of the following tasks:
Download source code here.
I decided to go into the benefits offered by a build automation system like Gradle. Let me make one thing clear before starting the details, this article is not a tutorial on Gradle, so I hope you're familiar with it before reading, although I confess that it is not very difficult to understand its main components. All code shown below is designed with Gradle 2.1 and Android Studio.
First you need to ensure the existence of the folder of assets to be included in the apk. So we will create the next task to verify the existence of that folder:
def assetsProjectFolderPath= '/src/main/assets'
task createAssetsFolder <<{ def folder = file("$assetsProjectFolderPath") if(!folder.exists()){ folder.mkdirs() } fileTree (dir: "$assetsProjectFolderPath").visit { def asset -> asset.file.delete() } }The createAssetsFolder task, guarantees the existence of the assets folder, and delete all the files it contains before starting the copy. To verify that above code works correctly, we can invoke the task from the command line in Terminal:
gradle createAssetsFolderNow we can create a folder for each of the stages of development that we want to use and copy each asset you want to include in our .apk file. To distinguish the included assets in each development environments we will use the variable env, that correspond to each of the previously created folders. For example, in the project I created a folder called assets, which has three folders, one for each of the stages of development : debug, staging and release.
We can begin to copy each asset file to the assets folder of our future apk , let´s call this task copyAssetFiles, note that the value of the assetsSourceFolderPath variable is the relative path of the assets folder to build.gradle file of your project:
def assetsSourceFolderPath = 'assets'
task copyAssetFiles(dependsOn: createAssetsFolder) << { fileTree( dir: "$assetsSourceFolderPath/$env").each{ asset -> copy{ from asset into "$assetsProjectFolderPath" } } }You can watch as stated dependence between copyAssetFiles and createAssetsFolder tasks. To run the copyAssetFiles task you need to specify the env parameter, like on the following command:
gradle copyAssetFiles -Penv=release
We are now ready to generate our .apk file with the assets already included, so we write the following task:
task assembleArtifact(dependsOn:[ clean,copyAssetFiles,assembleRelease ]) << { println "Assembled artifact for $env environment." }
This is not enough, since the tasks of which depends assembleArtifact be executed before the same but in any order. Therefore, it is possible that the assembleRelease task runs without is executed before the copyAssetFiles task, so we need to specify the order of precedence of them.
copyAssetFiles.mustRunAfter clean assembleRelease.mustRunAfter copyAssetFiles
We are ready to configure our TeamCity. On Gradle step options, we have a Gradle Parameters section, you need specify on Gradle tasks option a task to invoke, in this case:
assembleArtifact -Penv=stagingor
assembleArtifact -Penv=release
We have achieved the main thing: The code works !!! But is not everything, we need to go beyond. The code does not look good, because we have two hard coded variables. Let's ask ourselves a question first. How Gradle knows wich is our assets folder for inclusion in the apk? If you open the file with extension *.iml, get the answer:
So let's parse this file with XmlPullParser to obtain the variable that we need. Let's take our code to another level, using one of the benefits of Groovy, which allows us to define our tasks as POGOs, YESS!!!!, as you readed, Plain Old Groovy Objects. When we write a code must try to make it more flexible and reusable as possible. Therefore we will implement a method to extract the value of an attribute will in an xml file.
class FindXmlElementAttributeTask extends DefaultTask{ @Input def inputFile, charset, elementName, attributeName, filters @TaskAction def action(){ ext.attributeValue = null
//...Parse xml file and set ext.attributeValue } }This is unlike what we saw!!!! We will explain in broad terms the main elements. This class extends DefaultTask class that represents the basis of the tasks that we had seen before. The @Input annotation indicates that these values are parameters of the class. The inputFile parameter represents the file to parse, charset is the name of the encoding of the file, elementName is the name of the element we're looking for, attributeName is the name of the attribute will find and filters is a map with matches between key and value of the item to be searched. The @TaskAction indicates the method to execute when the task is invoked. The expression ext.attributeValue it is merely a property of the task.
You can set the task to find the path of assets folder as:
task findAssetsFolderPath(type: FindXmlElementAttributeTask){ inputFile file("$project.name" + ".iml") charset 'utf-8' elementName 'option' attributeName 'value' filters = [ name : 'ASSETS_FOLDER_RELATIVE_PATH'] }
The expression $project.name return the name of your project. If we want the path of AndroidManifest.xml file, only have to declare the task as follow:
task findManifestPath(type: FindXmlElementAttributeTask){ inputFile file("$project.name" + ".iml") charset 'utf-8' elementName 'option' attributeName 'value' filters = [ name : 'MANIFEST_FILE_RELATIVE_PATH'] }
Let's redefine the copyAssetFiles task:
task copyAssetFiles(dependsOn: findAssetsFolderPath) << { def folder = file(findAssetsFolderPath.attributeValue) if(!folder.exists()){ folder.mkdirs() } fileTree (dir: findAssetsFolderPath.attributeValue)
.visit { def asset -> asset.file.delete() } fileTree( dir: "assets/$env").each{ asset -> copy{ from asset into findAssetsFolderPath.attributeValue } } }
Now we are ready, in the Gradle Settings of your Continuous Integration, invoke one of the following tasks:
- assembleArtifact -Penv=release (To deploy with production assets)
- assembleArtifact -Penv=staging (To deploy with staging assets)
Download source code here.
Comments
Post a Comment