Setting process.env variable from Android Studio flavors - android

I want to be able to access a variable in my metro.config.js.
On iOS this can be achieved by adding a User Defined variable in the Build Settings tab. This is accessible with process.env.MY_VAR. The value of MY_VAR can be changed depending on which target is being built.
How can I achieve the same for Android using product flavours?
Here is my current setup
flavorDimensions "apps"
productFlavors {
free {
dimension "apps"
}
paid {
dimension "apps"
}
}
Execution Steps
When running iOS, I can see from the logs that the project gets built before the Packager starts
info Found Xcode workspace "foo.xcworkspace"
info Building (using "xcodebuild -workspace foo.xcworkspace -configuration Debug -scheme foo -destination id=CE408502-CD8C-4467-AE3D-295081CAF132 -derivedDataPath build/foo") <-- Build
▸ Running script '[CP-User] Config codegen'
▸ Compiling ReactNativeConfig.m
▸ Building library libreact-native-config.a
▸ Running script 'Upload Debug Symbols to Sentry'
▸ Running script 'Start Packager <-- Metro starts here
However, the packager starts before my gradle build task executes
info Starting JS server... <-- Metro starts here
info Installing the app...
> Configure project :app
Reading env from: .env
> Task :app:installFreeDebug <-- Build

Add the following codes to your build.gradle file.
buildTypes.each {
it.buildConfigField 'String', 'KEY_STRING', '"Default_Value"'
it.buildConfigField 'int', 'KEY_INT', '0'
}
applicationVariants.all { variant ->
if (variant.productFlavors[0].ext.has("key_1")) {
buildConfigField('String', 'KEY_STRING', variant.productFlavors.get(0).ext.key_1)
}
if (variant.productFlavors[0].ext.has("key_2")) {
buildConfigField('int', 'KEY_INT', variant.productFlavors.get(0).ext.key_2)
}
}
and then add ext.key_1 = '"desired string value"' and ext.key_2 = 'desired integer value' to flavors like this
flavorDimensions "apps"
productFlavors {
free {
dimension "apps"
ext.key_1 = '"free_value"'
ext.key_2 = '0'
}
paid {
dimension "apps"
ext.key_1 = '"paid_value"'
ext.ket_2 = '100'
}
}
Now you will access to the values in your app by calling BuildConfig.KEY_STRING and BuildConfig.KEY_INT.
If you don't set value for key_1 in a flavor, then the default value will be "Default_Value".
Update
I'm ashamed to say my above answer is wrong because in this way you can access the BuildConfig in JS scope but as you mentioned in your question you need to access the configuration in Metro Bundler and metor.config.js. I added the console.log(process.env); at the beginning of metro.config.js file and figured out process.env in Metro Bundler is my OS (in my case Windows 10) environment variables. So it means if you add desired variables as OS environment variable, then you will access them in metro.config.js through process.env. I tried to add environment variables through build.gradle but it wasn't a good way because:
1. I couldn't find a way to add the environment variable in Gradle
2. If I could do step 1, then as you mentioned the Metro starts before android Build, so the configuration will be ready late and Metro can't access them.
So, I decided to solve this issue in another way. First, I changed the build.gradle to log the required variables during Build process.
flavorDimensions "apps"
productFlavors {
free {
dimension "apps"
// Add any configuration you need for free flavor
buildConfigField('String', 'KEY_STRING', '"free_value"')
}
paid {
dimension "apps"
// Add any configuration you need for paid flavor
buildConfigField('String', 'KEY_STRING', '"paid_value1"')
buildConfigField('int', 'KEY_INT', '100')
}
}
applicationVariants.all { variant ->
// Read all flavors configuration
variant.productFlavors.each { flavor ->
flavor.buildConfigFields.each { key, value ->
// Set configuration to variant to be accessible through BuildConfig
variant.buildConfigField(value.type, value.name, value.value)
// Log all configurations in output
println "[" + variant.name + "]---" + value.name + "=" + value.value
}
}
variant.outputs.each { output ->
// For each separate APK per architecture, set a unique version code as described here:
// https://developer.android.com/studio/build/configure-apk-splits.html
def versionCodes = ["armeabi-v7a": 1, "x86": 2, "arm64-v8a": 3, "x86_64": 4]
def abi = output.getFilter(OutputFile.ABI)
if (abi != null) { // null for the universal-debug, universal-release variants
output.versionCodeOverride =
versionCodes.get(abi) * 1048576 + defaultConfig.versionCode
}
}
}
Then I wrote a bat file to run {variant}PreBundle task (For example freeDebugPreBundle) in Gradle. Then it will read the output and extract required variables and then set them as environment variables and finally call react-native run-android --variant {variant} command.
run-android.bat
#echo off
SETLOCAL ENABLEDELAYEDEXPANSION
:: Reading input arguments to extract desired variant
FOR %%a IN (%*) do (
set "arg=%%a"
if "!found!"=="true" (
set "variant=%%a"
GOTO:EndOfLoop
)
if "!arg!"=="--variant" set found=true
)
:EndOfLoop
:: User must provide variant because when you have flavors, then `debug` varinat does not exist anymore
if "!variant!"=="" (
echo You must provide --variant
GOTO:EOF
)
:: Creating a pttern to extract required variables
set search=[!variant!]---
:: Creating gradle task name based on input variant
set firstCharUpper=%variant:~0,1%
CALL :UpCase firstCharUpper
set taskName=build!firstCharUpper!%variant:~1%PreBundle
echo Running !taskName!
:: Creating an empty bat file to store environment variables and call later
echo. > custome_env.bat
:: Running gradle task and extracting required variables from output log
cd android
FOR /F "tokens=* USEBACKQ" %%F IN (`gradlew app:!taskName!`) DO (
SET "line=%%F"
SET "newLine=!line:%search%=!"
if not "!line!"=="!newLine!" (
echo !line!
set "keyValue=!line:%search%=!"
:: Adding environment variabel to custome_env.bat file
echo set !keyValue!>>../custome_env.bat
)
)
ENDLOCAL
:: Running custome_env.bat file to set required variables as environment variable
call custome_env.bat
:: Calling react-native command with all input parameters (%*)
call react-native run-android %*
GOTO:EOF
#echo on
:UpCase
:: Subroutine to convert a variable VALUE to all UPPER CASE.
:: The argument for this subroutine is the variable NAME.
FOR %%i IN ("a=A" "b=B" "c=C" "d=D" "e=E" "f=F" "g=G" "h=H" "i=I" "j=J" "k=K" "l=L" "m=M" "n=N" "o=O" "p=P" "q=Q" "r=R" "s=S" "t=T" "u=U" "v=V" "w=W" "x=X" "y=Y" "z=Z") DO CALL SET "%1=%%%1:%%~i%%"
GOTO:EOF
For running the application you should run this command:
run-android.bat --variant freeDebug
You can use other react-native options as well but you must provide --variant because when you have flavors, default variant debug does not exist.
PS: I don't know how is your release proceseduar, so I'm not sure this answer is compatible with your release procedure or not. if I know it maybe I can find a solution for it.

Related

Firebase App Distribution with apk splits unable to find apk

I'm trying to bend firebase app distribution to work with apk splits.
I almost have it, however my issue is this
Could not find the APK. Make sure you build first by running ./gradlew assemble[Variant],
or set the apkPath parameter to point to your APK
My task
task firebaseAllEnvRelease() {
group = "publishing"
dependsOn ordered(
":printVersionCode",
":foo:app:assembleAllRelease"
":foo:app:firebasePublishAllEnvRelease")
}
For whatever reason, the firebase task runs the apk check (not upload) beforehand, before assemble, so obviously the apk is not there -- how can I force it to respect the order of tasks?
I know gradle creates the tasks graph hopwever it likes, but I do have a utility ordered for what, which chains them via mustRunAfter and it is for sure correct.
Plan b is to run the assemble ina separate gradlew command before that, that works but -- why :/
The problem is that the gradle plugin
doesn't declare dependency on assemble task (in general, regardless of apk splits, by gradle convention, you shouldn't just "expect" the apks to be there)
doesn't generate tasks per apk splits -- but you do for flavors
So here is the work around for it:
// Generate firebase app distribution task variants for all abis
applicationVariants.all { variant ->
variant.outputs.all { output ->
def abi = output.getFilter(com.android.build.OutputFile.ABI)
if (abi == null) return
def abiName = abi.replace("_", "").replace("-", "")
task("appDistributionUpload${abiName.capitalize()}${variant.name.capitalize()}", type: com.google.firebase.appdistribution.gradle.UploadDistributionTask_Decorated) {
appDistributionProperties = new com.google.firebase.appdistribution.gradle.AppDistributionProperties(
new com.google.firebase.appdistribution.gradle.AppDistributionExtension(),
project,
variant
)
appDistributionProperties.apkPath = output.outputFile.absolutePath
appDistributionProperties.serviceCredentialsFile = project.file("secrets/ci-firebase-account.json")
appDistributionProperties.releaseNotes = abi
appDistributionProperties.groups = "ra-testers"
// Add dependsOn respective assemble task, so it actually
// builds apk it wants to upload, not just expect it to be there
dependsOn "assemble${variant.name.capitalize()}"
}
}
}

Android gradle Upload NDK symbols on every build

I want to upload NDK symbols on every build i do,
Under my Android inside gradle i use to have:
applicationVariants.all { variant ->
def variantName = variant.name.capitalize()
println("symbols will be added on varinat ${variantName}")
def task = project.task("ndkBuild${variantName}")
task.finalizedBy project.("uploadCrashlyticsSymbolFile${variantName}")
}
this does not compile anymore since i moved to FireBase :
Could not get unknown property 'uploadCrashlyticsSymbolFile
I don't see this task running.
I basiclly need this task to run on every build:
./gradlew app:assembleBUILD_VARIANT\
app:uploadCrashlyticsSymbolFileBUILD_VARIANT
Add this at the bottom of app's build.gradle outside android { ... } block.
afterEvaluate {
android.applicationVariants.all { variant ->
def variantName = variant.name.capitalize()
println("symbols will be added on variant ${variantName}")
def task = tasks.findByName("assemble${variantName}")
def uploader = "uploadCrashlyticsSymbolFile${variantName}"
// This triggers after task completion
task?.finalizedBy(uploader)
// This ensures ordering
task?.mustRunAfter(uploader)
}
}
You can try without afterEvaluate block. It should still work.
Likely you'd need to use Firebase App Distribution, which permits automatic upload of release build artifacts - and if you have the artifact with the matching debug symbols, they could actually be used - without the matching assembly, the symbols are somewhat irrelevant.
Number 1 is obviously a wrongful assumption, because the documentation clearly states:
./gradlew app:assembleBUILD_VARIANT app:uploadCrashlyticsSymbolFileBUILD_VARIANT
And this is already answered here.
In order to always upload, one can create a task dependency:
assembleRelease.finalizedBy uploadCrashlyticsSymbolFileRelease
This may require setting unstrippedNativeLibsDir and strippedNativeLibsDir.

React Native - Automatic version name from package.json to android build manifest

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()
}
}

Process resources for android gradle production Flavors

I'm looking for the way how to load my properties per product flavor.
I do have right now:
productFlavors {
flavor1 {
println 'Flavor 1'
loadProperties('flavor1.properties')
}
flavor2 {
println 'Flavor 2'
loadProperties('flavor2.properties')
}
I'm using that properties to filter/process some resources before build.
If I do configure my build for one flavor, and run it through Android studio - I could find that two flavors executed and there are two println in console:
Flavor 1
Flavor 2
Is that possible somehow to find active product flavor per build?
Something like get active (or current) product flavor and get access to all properties there?
Or even find a way to process resources per different flavors?
Or may be define some property underFlovors and re use it in another place?
Found the way how to get current build flavor:
android.applicationVariants.all{ variant ->
variant.processResources.doFirst {
println '' + project.getName() + ', flavorName: ' + flavorName
//Processing my resources there
}
}

Execute .bat from Android gradle after APK build

I`m moving my project to Gradle build system. After APK build I need to sign it with manufacturer certificate.
How to execute .bat file by Gradle after APK was built?
task runSign(type:Exec) {
println "Sign apk..."
commandLine = ['cmd','/c','sign.bat']
}
I know just how to run .bat before build (but I need after):
preBuild.doLast {
runSign.execute()
}
I've found the solution.
Go to Run -> Edit Configurations...
Choose module where you want to run task after APK build. Add new configuration after "Gradle-aware Make".
Click on icon at picture below to choose module where task is implemented and write name of it.
After this steps your custom Gradle task will be executed after APK build.
I needed to perform something similar, but additionally to that I needed to know with which product favour was built, and with what configuration.
I've ended up adding following line into build.gradle:
android {
applicationVariants.all { variant -> variant.assemble.doLast { signAndInstall.execute() } }
...
And with following helper function:
//
// Returns array for CommandLine, path, variant (arm7), configuration (debug / release)
//
def getCommandLine(path)
{
String taskReqStr = getGradle().getStartParameter().getTaskRequests().toString()
Pattern pattern = Pattern.compile("(assemble|generate)(\\w+)(Release|Debug)")
Matcher matcher = pattern.matcher(taskReqStr)
if (!matcher.find())
return [ path ]
String flavor = matcher.group(2).toLowerCase() + " " + matcher.group(3).toLowerCase()
return [ path, matcher.group(2).toLowerCase(), matcher.group(3).toLowerCase() ]
}
task signAndInstall(type: Exec) {
def batch = projectDir.toString() + '\\postbuild.bat'
commandLine = getCommandLine(batch)
}
With following postbuild.bat:
#echo off
rem echo %0 %*
if %1. == . exit /b 0
if %2. == . exit /b 0
set InPath=%~dp0build\outputs\apk\%1\%2\app-%1-%2.apk
set OutPath=%~dp0build\outputs\apk\app-%1-%2.apk
copy /y %InPath% %OutPath% 1>NUL
You can of course configure this batch to perform anything what you like, %1 receives your product favour (e.g. arm7, arm8, fat...), and %2 receives 'debug' or 'release' as configuration.

Categories

Resources