Skip to content

Commit

Permalink
feat: add incremental android build
Browse files Browse the repository at this point in the history
  • Loading branch information
rams23 committed Feb 28, 2023
1 parent 2fac9a8 commit 117340b
Show file tree
Hide file tree
Showing 9 changed files with 185 additions and 20 deletions.
77 changes: 77 additions & 0 deletions apktool
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#!/bin/bash
#
# Copyright (C) 2007 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# This script is a wrapper for smali.jar, so you can simply call "smali",
# instead of java -jar smali.jar. It is heavily based on the "dx" script
# from the Android SDK

# Set up prog to be the path of this script, including following symlinks,
# and set up progdir to be the fully-qualified pathname of its directory.
prog="$0"
while [ -h "${prog}" ]; do
newProg=`/bin/ls -ld "${prog}"`

newProg=`expr "${newProg}" : ".* -> \(.*\)$"`
if expr "x${newProg}" : 'x/' >/dev/null; then
prog="${newProg}"
else
progdir=`dirname "${prog}"`
prog="${progdir}/${newProg}"
fi
done
oldwd=`pwd`
progdir=`dirname "${prog}"`
cd "${progdir}"
progdir=`pwd`
prog="${progdir}"/`basename "${prog}"`
cd "${oldwd}"

jarfile=apktool.jar
libdir="$progdir"
if [ ! -r "$libdir/$jarfile" ]
then
echo `basename "$prog"`": can't find $jarfile"
exit 1
fi

javaOpts=""

# If you want DX to have more memory when executing, uncomment the following
# line and adjust the value accordingly. Use "java -X" for a list of options
# you can pass here.
#
javaOpts="-Xmx512M -Dfile.encoding=utf-8"

# Alternatively, this will extract any parameter "-Jxxx" from the command line
# and pass them to Java (instead of to dx). This makes it possible for you to
# add a command-line parameter such as "-JXmx256M" in your ant scripts, for
# example.
while expr "x$1" : 'x-J' >/dev/null; do
opt=`expr "$1" : '-J\(.*\)'`
javaOpts="${javaOpts} -${opt}"
shift
done

if [ "$OSTYPE" = "cygwin" ] ; then
jarpath=`cygpath -w "$libdir/$jarfile"`
else
jarpath="$libdir/$jarfile"
fi

# add current location to path for aapt
PATH=$PATH:`pwd`;
export PATH;
exec java $javaOpts -jar "$jarpath" "$@"
Binary file added apktool.jar
Binary file not shown.
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"@react-native-community/cli": "^10.2.0",
"@react-native-community/cli-platform-android": "^10.2.0",
"@react-native-community/cli-tools": "^10.1.1",
"@react-native-community/cli-config": "^10.1.1",
"@types/chai": "^4",
"@types/mocha": "^9.0.0",
"@types/node": "^16.18.12",
Expand Down Expand Up @@ -63,7 +64,8 @@
"posttest": "npm run lint",
"prepack": "npm run build && oclif manifest && oclif readme",
"test": "mocha --forbid-only \"test/**/*.test.ts\"",
"version": "oclif readme && git add README.md"
"version": "oclif readme && git add README.md",
"run": "./bin/dev run -a"
},
"engines": {
"node": ">=12.0.0"
Expand Down
90 changes: 81 additions & 9 deletions src/application/buildAndroid.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
import path from 'path'
import {executeCommand, getAppName, getProjectRootDir, getRootDestinationFolder} from './utils'
import {
executeCommand,
getApkToolExecutable,
getAppName,
getProjectRootDir,
getRootDestinationFolder,
getUberSignJava,
} from './utils'
import {getAndroidFlavors} from './config'
import {getAppBuildFolder} from './androidUtils'
import fs from 'fs'
import {findBestApkInFolder} from './runAndroid'

function capitalize(str: string) {
return str
}

export function buildAndroid(buildType?: string) {
export function buildAndroid(buildType?: string, incrementalBuild: boolean = false) {
// todo improve flavor management with debug and release and no flavor usage
const buildFlavor = getAndroidFlavors(buildType)
if (!buildFlavor) {
Expand All @@ -18,16 +27,79 @@ export function buildAndroid(buildType?: string) {
const androidFolder = path.join(getProjectRootDir(), 'android')
// todo handle Debug/release

const gradleBuildTask = (gradleFlavor && gradleFlavor !== 'debug') ? `install${capitalize(gradleFlavor)}Debug` : 'installDebug'
if (incrementalBuild) {
// todo check previous build existance
// create a temp directory
const tempDir = `${getRootDestinationFolder()}/tmp_${(Math.random()*1000).toFixed(0)}`;
const tempApkDir = `${tempDir}/extracted_apk`;

executeCommand(`${androidFolder}/gradlew \
const tempAssetBundle = path.join(tempDir, 'bundle_output')
fs.mkdirSync(tempAssetBundle, {recursive: true});

const bundleOutput = path.join(tempDir, 'bundle_output/index.android.bundle')
const resOutput = path.join(tempDir, 'bundle_output/res')

fs.mkdirSync(tempApkDir, {recursive: true});
fs.mkdirSync(tempAssetBundle, {recursive: true});

const apkPath = findBestApkInFolder(getAppBuildFolder(buildType));

const newApkPath = path.join(tempDir, 'new_build.apk');
const newSignedApkDir = path.join(tempDir, 'new_signed_apk');

executeCommand([getApkToolExecutable(), 'd', apkPath, '-o', tempApkDir, '-f'].join(' '));
executeCommand(`react-native bundle \
--platform android \
--dev false \
--entry-file index.js \
--bundle-output ${bundleOutput} \
--assets-dest ${resOutput}`);

if(!fs.existsSync(path.join(tempApkDir, 'assets'))){
fs.mkdirSync(path.join(tempApkDir, 'assets'));
}

// todo check bundle name
executeCommand(`cp -R ${tempAssetBundle}/* ${tempApkDir}/assets/`);
executeCommand(`cp -R ${tempAssetBundle}/res/* ${tempApkDir}/res/`);
executeCommand([getApkToolExecutable(), 'b', tempApkDir, '-o', newApkPath].join(' '));

/*
params for the signature
--ks c/Users/rams23/git/test/newRNApp/android/app/debug.keystore
--ksAlias androiddebugkey
--ksKeyPass android
--ksPass android
*/

// resign apk and align with uber-apk-signer
executeCommand(
`java -jar ${getUberSignJava()} \
-a ${newApkPath} \
-o ${newSignedApkDir}`
);

const apkFileName = fs.readdirSync(newSignedApkDir).find(f=>f.endsWith('.apk'));
if(!apkFileName){
throw new Error(`No apk found in the new signed apk folder ${newSignedApkDir}`);
}
const signedApkPath = path.join(newSignedApkDir, apkFileName);
executeCommand(`cp ${apkPath} ${apkPath.replace('.apk', '_old.apk')}`);
executeCommand(`cp ${signedApkPath} ${apkPath}`);

} else {
const gradleBuildTask = (gradleFlavor && gradleFlavor !== 'debug') ? `install${capitalize(gradleFlavor)}Debug` : 'installDebug'

executeCommand(`${androidFolder}/gradlew \
-Duser.dir=${androidFolder} \
app:${gradleBuildTask} \
`, {stdio: 'inherit'})

const destinationDir = getAppBuildFolder(buildType)
executeCommand(`rm -rf ${destinationDir} && mkdir -p ${destinationDir}`)
const destinationFlavorFolder = (gradleFlavor && gradleFlavor !== 'debug') ? `${gradleFlavor}/debug` : 'debug'
const gradleOutputDir = `${androidFolder}/app/build/outputs/apk/${destinationFlavorFolder}/`
executeCommand(`cp -R ${gradleOutputDir} ${destinationDir}`)
const destinationDir = getAppBuildFolder(buildType)
executeCommand(`rm -rf ${destinationDir} && mkdir -p ${destinationDir}`)
const destinationFlavorFolder = (gradleFlavor && gradleFlavor !== 'debug') ? `${gradleFlavor}/debug` : 'debug'
const gradleOutputDir = `${androidFolder}/app/build/outputs/apk/${destinationFlavorFolder}/`
executeCommand(`cp -R ${gradleOutputDir} ${destinationDir}`)
}

}
2 changes: 1 addition & 1 deletion src/application/runAndroid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ function checkBuildPresent(buildType?: string) {
return fs.existsSync(appPath)
}

function findBestApkInFolder(dir: string, arc?: string) {
export function findBestApkInFolder(dir: string, arc?: string) {

const files = fs.readdirSync(dir)
// todo debug
Expand Down
17 changes: 14 additions & 3 deletions src/application/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export function executeCommand(command: string, options?: ExecSyncOptionsWithBuf
}

export function getRootDestinationFolder() {
return path.join(getProjectRootDir(), 'app_builds')
return path.join(getProjectRootDir(), '.rn-incremental')
}

export function getProjectRootDir() {
Expand All @@ -24,7 +24,18 @@ export function getAppName() {
return packageJson.name
}

export function sleep(ms :number) {
return new Promise((resolve) => setTimeout(resolve, ms));
export function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms))
}

export function getRootModuleDir() {
return path.join(__dirname, '..', '..')
}

export function getApkToolExecutable() {
return path.join(getRootModuleDir(), 'apktool')
}

export function getUberSignJava() {
return path.join(getRootModuleDir(), 'uber-apk-signer.jar')
}
13 changes: 7 additions & 6 deletions src/commands/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export default class Build extends Command {
android: Flags.boolean({char: 'a', description: 'Generate android native build'}),
all: Flags.boolean({description: 'Generate both ios and android native build'}),
flavor: Flags.string({char: 'f', description: 'Specify flavor to build'}),
incremental: Flags.boolean({char: 'I', description: 'Incremental build', default: false}),
}

static args = {
Expand All @@ -26,20 +27,20 @@ export default class Build extends Command {
public async run(): Promise<void> {
const {args, flags} = await this.parse(Build)

const shouldBuildAndroid = flags.android ?? flags.all;
const shouldBuildIos = flags.ios ?? flags.all;
const buildFlavor = flags.flavor;
const shouldBuildAndroid = flags.android ?? flags.all
const shouldBuildIos = flags.ios ?? flags.all
const buildFlavor = flags.flavor

this.log('Build app', getAppName());
this.log('Build app', getAppName())

if (shouldBuildIos) {
this.log('Build ios')
buildIos(buildFlavor, 'simulator');
buildIos(buildFlavor, 'simulator')
}
if (shouldBuildAndroid) {
this.log('Building android')
// build release and debug?
buildAndroid(buildFlavor);
buildAndroid(buildFlavor, flags.incremental)
}
}
}
2 changes: 2 additions & 0 deletions src/commands/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import {Args, Command, Flags} from '@oclif/core'
import {runApp as runAndroid} from '../application/runAndroid'
import {runApp as runIos} from '../application/runIos'
import {startMetro, checkIsMetroRunning} from '../application/metroManager'
import loadConfig from '@react-native-community/cli-config'
import {getProjectRootDir} from '../application/utils'

export default class Run extends Command {
static description = 'Run the native app'
Expand Down
Binary file added uber-apk-signer.jar
Binary file not shown.

0 comments on commit 117340b

Please sign in to comment.