Create a gist now

Instantly share code, notes, and snippets.

What would you like to do?
A crazy script to extract RN BuildConfig statements from a gradle file because this alone didn't justify a day learning Groovy 😬
#!/usr/bin/env node
'use strict';
var beautify = require('json-beautify');
var fs = require('fs');
var ip = require('ip');
var strftime = require('strftime');
var Handlebars = require('handlebars');
var buildType = process.argv[process.argv.length - 1]
// See: http://rubular.com/r/nLTmEz4MJ4
var BUILD_CONFIG_FIELD_REGEX = /^\s*buildConfigField\s+["']([\w]*)["'],\s*["']([^"']*)["'],\s*"(.*)["']\s*$/
function unescapeMatch(value, type) {
switch (type) {
case 'String':
return value.substring(2, value.length - 2);
break;
case 'int':
return parseInt(value);
break;
case 'boolean':
var asLower = value.toLowerCase();
return asLower === 'true' || asLower === 't';
break;
}
}
// Crazy. Match braces and find statements that look like BuildConfig.
// Ideally android/app/build.gradle would use this config and then
// export it to json on build so that java and javascript both have
// access to config vars.
function extractGradleConfig (gradleFile) {
var match, leftBracketCount, rightBracketCount, line, i, buildConfigFieldMatch;
var buildGradle = fs.readFileSync(gradleFile, 'utf8');
var lines = buildGradle.split(/\n/);
var config = {}
var insideBuildTypes, bracketDepth;
for (line, i = 0, bracketDepth = 0; i < lines.length; i++) {
line = lines[i];
if (line.match(/buildTypes/)) {
insideBuildTypes = true;
}
if (insideBuildTypes) {
leftBracketCount = (line.match(/{/g) || []).length
rightBracketCount = (line.match(/}/g) || []).length
bracketDepth += leftBracketCount - rightBracketCount;
if (bracketDepth === 2 && (match = line.match(/(\w*)\s*{/))) {
var section = match[1]
config[section] = config[section] || {}
}
if ((buildConfigFieldMatch = line.match(BUILD_CONFIG_FIELD_REGEX))) {
config[section][buildConfigFieldMatch[2]] = unescapeMatch(buildConfigFieldMatch[3], buildConfigFieldMatch[1])
}
}
if (insideBuildTypes && bracketDepth === 0) {
insideBuildTypes = false;
}
}
return config[buildType];
}
// Hopefully self-explanatory. Extract the config!
var conf = extractGradleConfig(__dirname + '/app/build.gradle');
// This section uses some custom logic to inject either your local
// ip address or the loopback interface, depending on the configuration.
// You'll have to edit this to suit your needs.
var host = conf['API_HOST']
if (!host || !conf['USE_API_HOST']) {
host = conf['USE_LOCALHOST'] ? 'localhost' : ip.address()
host = 'http://' + host + ':3000/api/v7'
}
conf['API_HOST'] = host;
// Make this js instead of json:
var output = 'module.exports = ' + beautify(conf, null, 2, 100);
// The output destination:
var outputFileName = 'environment.android.js';
// This would need to be the location of your app's settings. For
// import into javascript:
var outputPath = __dirname + '/../app/config/' + outputFileName;
// XML template path so that environment variables get injected into
// strings.xml. See below example. Modify to suit yoru needs, obv.
var stringsXMLTemplatePath = __dirname + '/strings.xml.handlebars';
var stringsXMLOutputPath = __dirname + '/app/src/main/res/values/strings.xml';
fs.readFile(stringsXMLTemplatePath, 'utf8', function (err, data) {
if (err) {
process.stderr.write('Unable to read strings.xml.handlebars:', err);
exit(1);
} else {
var template = Handlebars.compile(data);
fs.writeFile(stringsXMLOutputPath, template(conf), function (err) {
if (err) {
process.stderr.write('Unable to write strings.xml:', err);
exit(1);
} else {
process.stderr.write('Wrote strings.xml to "' + stringsXMLOutputPath.trim() + '"\n');
}
});
}
})
// Output to XML:
output = '// -----------------------------------------\n' +
'// Auto generated file\n' +
'// Created ' + strftime('%c', new Date()) + '\n' +
'// To modify config, edit android/app/build.gradle\n' +
'// -----------------------------------------\n\n' + output;
fs.writeFile(outputPath, output, function(err) {
if (err) {
console.log('Exited because:',err);
process.exit(1);
} else {
process.stderr.write('Wrote config to "' + outputPath.trim() + '"\n');
}
});
# android/app/react.gradle
# Add applyConfig and line 18 toward the bottom of this file:
...
enabled config."bundleIn${targetName}" ||
config."bundleIn${buildTypeName.capitalize()}" ?:
targetName.toLowerCase().contains("release")
}
def applyConfig = tasks.create(
name: "apply${buildTypeName.capitalize()}Config",
type: Exec) {
commandLine "../extract-gradle-config.js", buildTypeName
}
currentBundleTask.dependsOn("apply${buildTypeName.capitalize()}Config")
// Hook bundle${productFlavor}${buildType}JsAndAssets into the android build process
currentBundleTask.dependsOn("merge${targetName}Resources")
currentBundleTask.dependsOn("merge${targetName}Assets")
...
<resources>
<string name="app_name">{{MY_APP_NAME}}</string>
<string name="filter_title_main">{{MY_APP_NAME}}</string>
<string name="facebook_app_id">{{FACEBOOK_APP_ID}}</string>
<string name="google_api_key">{{GOOGLE_API_KEY}}</string>
</resources>
Owner

rreusser commented May 13, 2016

Designed to work on android/app/build.gradle files with a block that looks like:

    buildTypes {
        debug {
            buildConfigField "String",  "WEB_HOST",                      "\"https://www.projectname.com\""
            buildConfigField "String",  "API_HOST",                      "\"http://192.168.20.146:3000/api/v7\""
            buildConfigField "String",  "CODE_HOST",                     "\"http://localhost:8081/index.android.bundle?platform=android&dev=true\""
            buildConfigField "boolean", "USE_API_HOST",                  "false"
            buildConfigField "boolean", "FORCE_LOCALHOST",               "false"
            buildConfigField "boolean", "USE_BUNDLED_JS",                "false"
            buildConfigField "boolean", "DEBUG",                         "true"
            buildConfigField "String",  "CODEPUSH_DEPLOYMENT_KEY",          "\"ASDFJKL\""

Inputs:

  • android/app/build.gradle with buildConfigField statements.
  • strings.xml.handlebars: a template for strings.xml

Output:

  • android/app/src/main/res/values/strings.xml with env vars substituted
  • app/config/environment.android.js so that you can access env vars from javascript
  • buildConfigField already makes these variables accessible in Java

NB: This is a hacky script that worked great. The extraction part should work alright in general, but don't expect it to work without fixing up the paths and details just a bit.

You can use React Native Config package for setting up configs based on environment

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment