Currently I have a react native app and the issue that I have is that is very time consuming to update the version on every build or commit.
Also, I have Sentry enabled so every time I build, some builds get the same version so some crashes are hard to determine where they came from.
Lastly, updating the version manually is error prone.
How can I setup my builds to generate an automatic version every time I build and forget about all of this manual task?
While the currently accepted answer will work, there is a much simpler, and therefore more reliable way to do it.
You can actually read the value set in package.json right from build.gradle.
Modify your android/app/build.gradle:
// On top of your file import a JSON parser
import groovy.json.JsonSlurper
// Create an easy to use function
def getVersionFromNpm() {
// Read and parse package.json file from project root
def inputFile = new File("$rootDir/../package.json")
def packageJson = new JsonSlurper().parseText(inputFile.text)
// Return the version, you can get any value this way
return packageJson["version"]
}
android {
defaultConfig {
applicationId "your.app.id"
versionName getVersionFromNpm()
}
}
This way you won't need a pre-build script or anything, it will just work.
Since I was working with this for several days, I decided to share with everyone how I did it, because it could help others.
Tools used:
GitVersion: We will use GitVersion to generate a semantic version automatically depending on many factors like current branch, tags, commits, etc. The toold does an excellent job and you can forget about naming your versions. Of course, if you set a tag to a commit, it will use that tag as name.
PowerShell: This command line OS built by Microsoft has the ability to be run from Mac, Linux or Windows, and I chose it because the builds can be agnostic of the OS version. For example I develop on Windows but the build machine has MacOS.
Edit App build.gradle
The app gradle only needs one line added at the end of it. In my case I have the Google Play Services gradle and I added it after that.
apply from: 'version.gradle'
version.gradle
This file should be in the same folder as your app gradle and this is the content:
task updatePackage(type: Exec, description: 'Updating package.json') {
commandLine 'powershell', ' -command ' , '$semver=(gitversion /showvariable Semver); Set-Content -path version.properties -value semver=$semver; npm version --no-git-tag-version --allow-same-version $semver'
}
preBuild.dependsOn updatePackage
task setVariantVersion {
doLast {
if (plugins.hasPlugin('android') || plugins.hasPlugin('android-library')) {
def autoIncrementVariant = { variant ->
variant.mergedFlavor.versionName = calculateVersionName()
}
if (plugins.hasPlugin('android')){
//Fails without putting android. first
android.applicationVariants.all { variant -> autoIncrementVariant(variant) }
}
if (plugins.hasPlugin('android-library')) {
//Probably needs android-library before libraryVariants. Needs testing
libraryVariants.all { variant -> autoIncrementVariant(variant) }
}
}
}
}
preBuild.dependsOn setVariantVersion
setVariantVersion.mustRunAfter updatePackage
ext {
versionFile = new File('version.properties')
calculateVersionName = {
def version = readVersion()
def semver = "Unknown"
if (version != null){
semver = version.getProperty('semver')
}
return semver
}
}
Properties readVersion() {
//It gets called once for every variant but all get the same version
def version = new Properties()
try {
file(versionFile).withInputStream { version.load(it) }
} catch (Exception error) {
version = null
}
return version
}
Now, let's review what the script is actually doing:
updatePackage: This task runs at the very beginning of your build (actually before preBuild) and it executes gitversion to get the current version and then creates a version.properties file which later be read by gradle to take the version.
setVariantVersion: This is called afterEvaluate on every variant. Meaning that if you have multiple builds like debug, release, qa, staging, etc, all will get the same version. For my use case this is fine, but you might want to tweak this.
Task Order: One thing that bothered me was that the version was being run before the file was generated. This is fixed by using the mustRunAfter tag.
PowerShell Script Explained
This is the script that gets run first. Let's review what is doing:
$semver=(gitversion /showvariable Semver);
Set-Content -path props.properties -value semver=$semver;
npm version --no-git-tag-version --allow-same-version $semver
Line 1: gitversion has multiple type of versions. If you run it without any parameter you will get a json file with many variants. Here we are saying that we only want the SemVer. (See also FullSemVer)
Line 2: PowerShell way to create a file and save the contents to it. This can be also made with > but I had encoding issues and the properties file was not being read.
Line 3: This line updates your package.json version. By default it saves a commit to git with the new version. --no-git-tag-version makes sure you don't override it.
And that is it. Now every time you make a build, the version should be generated automatically, your package.json updated and your build should have that specific version name.
App Center
Since I am using App Center to make the builds, I will tell you how you can use this in a Build machine. You only need to use a custom script.
app-center-pre-build.sh
#!/usr/bin/env sh
#Installing GitVersion
OS=$(uname -s)
if [[ $OS == *"W64"* ]]; then
echo "Installing GitVersion with Choco"
choco install GitVersion.Portable -y
else
echo "Installing GitVersion with Homebrew"
brew install --ignore-dependencies gitversion
fi
This is needed because GitVersion is not currently a part of the build machines. Also, you need to ignore the mono dependency when installing, otherwise you get an error when brew tries to link the files.
The #MacRusher version was fine for me. Just for further readers, I had to add .toInteger() to make it work. Since I'm using yarn version --patch to automatically upgrade the version in package.json I also had to take only the two first characters.
Here is the new version:
// On top of your file import a JSON parser
import groovy.json.JsonSlurper
def getVersionFromPackageJson() {
// Read and parse package.json file from project root
def inputFile = new File("$rootDir/../package.json")
def packageJson = new JsonSlurper().parseText(inputFile.text)
// Return the version, you can get any value this way
return packageJson["version"].substring(0,2).toInteger()
}
android {
defaultConfig {
applicationId "your.app.id"
versionName getVersionFromPackageJson()
}
}
Related
My question is similar to this one, except that in that question, he wanted to pass arguments directly from gradle to CMake. But in my case, I am not directly invoking the cmake build, but rather "flutter is doing it for me"
So to put it all together, when the app build starts:
gradle build of the app starts (build.gradle in the flutter project) ------(1)
the above build invokes gradle build of the plugin (build.gradle in the plugin's project) ------(2)
the gradle build of the plugin invokes the CMake build of the plugin (CMakeLists.txt in the plugin project) ------(3)
Now sending args from step (2) to step (3) is as mentioned in the link above.
My question is how to send args from step (1) to step (2)?
I am not very experienced with gradle but I tried inspecting the build.gradle file of step (1) to see from where it is invoking the build.gradle of the plugin, but I wasn't able to do that.
Update:
this answer is very similar to what I want, except that, as I mentioned, I want to pass variable from a gradle build that indirectly invokes cmake.
The use case for me is as follows: the plugin has some C code and uses shared libraries, these libraries have different versions and the user must choose one of them at build time, thats why I want the user to pass variables to cmake, for ex:
externalNativeBuild {
cmake {
arguments "-DLIB_VERSION=VERSION_WITH_SOME_OPTION"
}
}
and then in CMake:
if(${LIB_VERSION})
# link some version of the shared libs
else()
# link another version
So I found a solution, but I don't know if this is the right way to do it, as it defines the variables to be passed as "global gradle variables".
So we have 3 files:
(1) android/app/build.gradle in the flutter app's project folder
(2) android/build.gradle in the plugin's project folder
(3) CMakeLists.txt in the plugin's project folder
We want to pass an argument from (1) to (3) (since the user of the plugin only has access to (1)):
in (1), define the variables to be passed, put this at the end of the file:
ext {
Q16 = 1
HDRI = 0
}
in (2), read the variables at the beginning of the file:
def Q8
def Q16
def HDRI
if(rootProject.hasProperty("Q8")){
Q8 = 1
}
else{
Q8 = 0
}
if(rootProject.hasProperty("Q16")){
Q16 = 1
}
else{
Q16 = 0
}
if(rootProject.hasProperty("HDRI")){
HDRI = 1
}
else{
HDRI = 0
}
Then pass them to (3) (since (2) directly invokes (3)):
android{
...
defaultConfig{
...
externalNativeBuild{
cmake {
arguments "-DQ8=${Q8}" , "-DQ16=${Q16}" , "-DHDRI=${HDRI}"
}
}
}
}
Now we can read them in (3):
message("from CMakeLists.txt: Q8 = ${Q8}")
message("from CMakeLists.txt: Q16 = ${Q16}")
message("from CMakeLists.txt: HDRI = ${HDRI}")
I have to add the Analytics tool Sentry to our Android project. In order to make it work, one needs to create mappings for the obfuscated code (from Proguard/R8) and upload it later to Sentry.
On the website https://docs.sentry.io/platforms/android/ it is even described how to do that.
There it is written that one needs to create a gradle task looking like this:
gradle.projectsEvaluated {
android.applicationVariants.each { variant ->
def variantName = variant.name.capitalize();
def proguardTask = project.tasks.findByName(
"transformClassesAndResourcesWithProguardFor${variantName}")
def dexTask = project.tasks.findByName(
"transformClassesWithDexFor${variantName}")
def task = project.tasks.create(
name: "processSentryProguardFor${variantName}",
type: Exec) {
workingDir project.rootDir
commandLine *[
"sentry-cli",
"upload-proguard",
"--write-properties",
"${project.rootDir.toPath()}/app/build/intermediates/assets" +
"/${variant.dirName}/sentry-debug-meta.properties",
variant.getMappingFile(),
"--no-upload"
]
}
dexTask.dependsOn task
task.dependsOn proguardTask
}
}
This shall wait until Proguard is finished, than copy this properties file to the assets. However, when I add this to my Android gradle script I get the error:
Could not create task
':app:processSentryProguardForPlayStoreStagingDebug'.
No signature of method: java.util.ArrayList.multiply() is applicable for argument types: (ArrayList) values: [[sentry-cli, upload-proguard,
--write-properties, {Application-Path}/app/build/intermediates/assets/playStoreStaging/debug/sentry-debug-meta.properties,
...]] Possible solutions: multiply(java.lang.Number),
multiply(java.lang.Number)
I assume there is something wrong with the multiplication symbol * before the commandLine array. But when I remove it I get the error
Could not create task
':app:processSentryProguardForPlayStoreStagingDebug'.
Cannot cast object 'sentry-cli' with class 'java.lang.String' to class 'int'
So I tried to test this with only that line
commandLine "sentry-cli", ...
Which gave me another error
What went wrong: Cannot invoke method dependsOn() on null object
Thus I assume something went really wrong with that gradle script since it seems the dependend task can't be found. Does anyone have any idea how to fix this (or optionally have any other idea how to copy that sentry-debug-meta.properties file to my assets in another way, once Proguard/R8 is finished)?
Thanks!
-------- EDIT --------
I noticed something important.
The gradle tasks are defined in a different name than what was defined in the manual. Looking at my tasks I have them named
transformClassesAndResourcesWithR8For...
and
transformClassesWithDexBuilderFor...
However, I print the variantName then for checking but it seems my tasks are incomplete.
In my tasks list there exist
transformClassesAndResourcesWithR8ForPlayStoreStagingDebug
but not
transformClassesAndResourcesWithR8ForPlayStoreStagingRelease
and thus the task can't be found. I think that is the real problem here. So where are these gradle tasks defined?
------- EDIT 2 --------
Okay I noticed something strange here. Some variants don't have tasks. It makes sense that DEBUG tasks don't have R8 tasks but I found this here:
Variant: PlayStoreStagingRelease DexTask is null
Variant: PlayStorePreviewRelease DexTask is null
Variant: HockeyAppRelease DexTask is null
Variant: LocalServerRelease DexTask is null
Variant: PlayStoreProductionRelease DexTask is null
So how can this be?
I'd recommend using the Sentry Gradle integration (Gradle plugin) which is described here https://docs.sentry.io/platforms/android/#gradle-integration
The official Android Gradle plugin changed its task names over versions, Gradle version also affects those code snippets.
Google also replaced Proguard with R8 and it also affected those code snippets.
Is there a reason why not using the Sentry Gradle integration? if so, We'll be looking into updating them.
Thanks.
java.util.ArrayList.multiply() hints for that * in front of the [ ] list, which looks strange to me. Try removing the *[ ], only keeping List<String> (there's no ArrayList expected, to begin with):
commandLine "sentry-cli", "upload-proguard", "--write-properties", "${project.rootDir.toPath()}/app/build/intermediates/assets/${variant.dirName}/sentry-debug-meta.properties", variant.getMappingFile(), "--no-upload"
You'd have to look up how your tasks are actually being called, but it should be something alike:
def r8Task = project.tasks.findByName("transformClassesAndResourcesWithR8For${variantName}")
def d8Task = project.tasks.findByName("transformClassesWithDexBuilderFor${variantName}")
With a null check, because not every variant might have minifyEnabled true set:
if(r8Task != null) {
d8Task.dependsOn task
task.dependsOn r8Task
}
Maybe even a previous null check is required, because variant.getMappingFile() needs R8.
And that some flavors have no D8 task might be based upon the absence of code (nothing to do).
Here's a summary of the steps that I followed for integrating Sentry with my Android app. These steps are to ensure the sentry gradle plugin works as expected and automatically uploads the proguard mapping files, without you having to worry about uploading using cli. I assume you would have setup the Sentry SDK as described here:
https://docs.sentry.io/platforms/android/#integrating-the-sdk
Ensure you have Android Studio gradle plugin 3.5.0 (Not 3.6.x, that seems to break the sentry plugin. I observed that the sentry proguard or native symbol upload tasks are not configured or executed at all). This value should be in your root project's build.gradle in dependencies block
Provide a sentry.properties file the root folder of your project. The sentry.properties file should have the following values at minimum:
defaults.project=your_sentry_project_name
defaults.org=your_sentry_org_name
auth.token=sentry_project_auth_token
You can get info about generating auth tokens here: https://sentry.io/settings/account/api/auth-tokens/
(Optional: If you have build flavors) In my case, I have different flavors for my app. So, I had to put the sentry.properties inside my flavor specific folder in /app/src/ folder. Then, I wrote a gradle task to copy the flavor specific sentry.properties file into the project's root folder during gradle's configuration phase. Example:
task copySentryPropertiesTask {
if (getBuildFlavor() != null && !getBuildFlavor().isEmpty()) {
println("Copying Sentry properties file: ${getBuildFlavor()}")
copy {
from "src/${getBuildFlavor()}/"
include "sentry.properties"
into "../"
}
}
}
def getBuildFlavor() {
Gradle gradle = getGradle()
String tskReqStr = gradle.getStartParameter().getTaskRequests().toString()
Pattern pattern;
if (tskReqStr.contains("assemble"))
pattern = Pattern.compile("assemble(\\w+)(Release|Debug)")
else
pattern = Pattern.compile("generate(\\w+)(Release|Debug)")
Matcher matcher = pattern.matcher(tskReqStr)
if (matcher.find())
return matcher.group(1)
else {
println "NO MATCH FOUND"
return ""
}
}
Note 1: You can place this task in your app/build.gradle anywhere (I had placed it at the end).
Note 2: If you followed step 3 for build flavors, you can also add the root folder's sentry.properties in .gitignore. Since, it will be copied everytime you create a build.
Sentry should now be able to upload the proguard files for any release builds (or if you set minifyEnabled=true for any buildType).
I have an Android Studio project which uses NDK and CMake and externalNativeBuild. To reduce packet size I have several flavors for different texture compression formats. There are no code changes, i.e. all the resulting APKs are using exactly the same code.
productFlavors {
ETC2 {
manifestPlaceholders = [supportedTexture: "GL_OES_compressed_ETC2_RGB8_texture"]
}
DXT {
manifestPlaceholders = [supportedTexture: "GL_EXT_texture_compression_dxt1"]
}
ATC {
manifestPlaceholders = [supportedTexture: "GL_AMD_compressed_ATC_texture"]
}
//...and list goes on...
}
What this means in practice is that I have a Copy task which checks the current flavor and copies the correct texture pack into the APK. The getCurrentFlavor() function is copied from How to get current flavor in gradle:
task copyTexSD(type: Copy) {
def currentFlavor = getCurrentFlavor()
if(currentFlavor == "etc2") {
from 'bin/tex/ETC2.bin'
}
else if(currentFlavor == "dxt") {
from 'bin/tex/DXT.bin'
}
else if(currentFlavor == "atc") {
from 'bin/tex/ATC.bin'
}
//...
into 'src/main/assets/tex'
}
To build averything I use the following batch command:
call gradlew clean
call gradlew assembleETC2Release
call gradlew assembleDXTRelease
call gradlew assembleATCRelease
This works otherwise well, but for some reason the texture packets which are copied to previous APKs are also included in the subsequent APKs like this:
app-ETC2-release.apk contains only ETC2.bin file
app-DXT-release.apk contains DXT.bin and ETC2.bin
app-ATC-release.apk contains ATC.bin, DXT.bin and ETC2.bin
Why the build process includes assets from previous Gradle task? How can I make the build process to have only one texture file per APK?
Somehow I had a wrong assumption that each gradle task would be independent. Of course all the file(s) that have been copied in the assets folder in previous tasks are still there if they are not explicitly deleted.
So, there seems to be two possibilities to get this to work:
1) Modify the batch file to delete data from the texture folder before calling next gradle task.
OR
2) Create delete task in gradle file which runs before copy task. Examples can be found here: Gradle - Delete files with certain extension .
Currently we have Bamboo listening to a Git repository on any changes. When a change occur the build process starts and increases the (Bamboo) build number by one.
I thought it would be nice to use this same build number for the Android project (versionCode) so that the user of the app can always refer to the actual build he/she received. This way the build number goes from 1 to 2 on the build server. Only the workstation doesn't know about this and still uses version 1.
Is there any way to sync this build number?
Tried:
One possible solution a friend of mine suggested was to use a git command to get the commit number: git rev-list HEAD --count which is awesome. The only downside of this is that you cannot properly change build number within Bamboo. So the build number of Bamboo should be leading.
Btw, I'm using Android Studio with Gradle
Bamboo can be configured to set the versionCode value in the manifest to the build number when building, so propagating this change back to where the development is happening should be unnecessary. To configure Bamboo to do this, add a Script task to the build plan (before the actual build task) with a body of:
sed -i 's/android:versionCode="[[:digit:]]*"/android:versionCode="${bamboo.buildNumber}"/' AndroidManifest.xml
Yes, it is quite easy with gradle. You have to retrieve the latest build result in the gradle script and put it in the versionCode.
Here it is what you need (remember to change the variables to refer your server):
def getBambooNumber(){
def url = "https://bambooServer:bambooPort/rest/api/latest/result/PROJECT-BAMBOO-ID/latest.json?os_authType=basic".toURL()
def authValue = "USER:PASSWORD".bytes.encodeBase64().toString()
def json = new JsonSlurper().parseText(url.getText(requestProperties : ["Authorization" : "Basic " + authValue]))
return json.buildNumber + 1 // +1 to Get the new build number
}
def bambooBuild = bambooBuildNumber()
android {
...
defaultConfig {
...
versionCode bambooBuild
//This allows you to access in BuildConfig to the bamboo build
buildConfigField "Integer", "BAMBOO_BUILD", "${bambooBuild}"
...
}
...
}
Let me know if this works for you.
We have lots of developers and lots of devices and lots of fresh and old versions of apk. Sometimes bad behaviour is occurred. I need to detect what version of the apk is installed on the device (what branch, what last commit, what files was changed from last commit, when apk criated).
We use Gradle to make apk from projects.
Is there any method (directive or plugin or script) to place the version information somewhere in the apk? For example, as constant in java class or as text file in resources.
You could use next snippet:
task gitInfo << {
ext.revision = getGitRevParseInfo ( "--short" )
ext.branch = getGitRevParseInfo ( "--abbrev-ref" )
println ext.revision
println ext.branch
}
def getGitRevParseInfo (what) {
def cmd = "git rev-parse " + what + " HEAD"
def proc = cmd.execute ()
proc.text.trim ()
}
So you can use ext for constructing versionName for android app