Skip to content

Commit

Permalink
feat: Allow configuring Gradle, AGP, various SDK and Kotlin versions …
Browse files Browse the repository at this point in the history
…for Espresso server (#496)
  • Loading branch information
tinder-ktarasov authored and mykola-mokhnach committed Oct 22, 2019
1 parent 87c9d0a commit a402a53
Show file tree
Hide file tree
Showing 10 changed files with 459 additions and 12 deletions.
10 changes: 6 additions & 4 deletions espresso-server/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'

android {
compileSdkVersion 28
buildToolsVersion '28.0.3'
compileSdkVersion getIntegerProperty('appiumCompileSdk', 28)
buildToolsVersion getStringProperty('appiumBuildTools', '28.0.3')
defaultConfig {
applicationId "io.appium.espressoserver"
minSdkVersion 18
targetSdkVersion 28
minSdkVersion getIntegerProperty('appiumMinSdk', 18)
targetSdkVersion getIntegerProperty('appiumTargetSdk', 28)
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
Expand Down Expand Up @@ -73,6 +73,8 @@ dependencies {
androidTestImplementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"

// additionalAppDependencies placeholder (don't change or delete this line)
}

tasks.withType(Test) {
Expand Down
14 changes: 12 additions & 2 deletions espresso-server/build.gradle
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.

buildscript {
ext.kotlin_version = '1.3.31'
ext.getIntegerProperty = {String propertyName, int defaultValue ->
Object propertyValue = project.getProperties().get(propertyName)
(propertyValue) ? Integer.parseInt(propertyValue.toString(), 10) : defaultValue
}
ext.getStringProperty = { String propertyName, String defaultValue ->
Object propertyValue = project.getProperties().get(propertyName)
(propertyValue) ? propertyValue.toString() : defaultValue
}

ext.kotlin_version = getStringProperty('appiumKotlin', '1.3.31')
ext.android_gradle_plugin_version = getStringProperty('appiumAndroidGradlePlugin', '3.4.2')

repositories {
maven {
Expand All @@ -12,7 +22,7 @@ buildscript {
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'com.android.tools.build:gradle:3.4.2'
classpath "com.android.tools.build:gradle:$android_gradle_plugin_version"

// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
Expand Down
6 changes: 6 additions & 0 deletions lib/desired-caps.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ let espressoCapConstraints = {
espressoServerLaunchTimeout: {
isNumber: true
},
espressoBuildConfig: {
isString: true
},
showGradleLog: {
isBoolean: true
}
};

let desiredCapConstraints = {};
Expand Down
2 changes: 2 additions & 0 deletions lib/driver.js
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,8 @@ class EspressoDriver extends BaseDriver {
appPackage: this.opts.appPackage,
appActivity: this.opts.appActivity,
forceEspressoRebuild: !!this.opts.forceEspressoRebuild,
espressoBuildConfig: this.opts.espressoBuildConfig,
showGradleLog: !!this.opts.showGradleLog,
serverLaunchTimeout: this.opts.espressoServerLaunchTimeout,
androidInstallTimeout: this.opts.androidInstallTimeout,
});
Expand Down
30 changes: 27 additions & 3 deletions lib/espresso-runner.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import { JWProxy } from 'appium-base-driver';
import { retryInterval } from 'asyncbox';
import logger from './logger';
import ServerBuilder from './server-builder';
import path from 'path';
import { fs, util, mkdirp } from 'appium-support';
import { version } from '../../package.json'; // eslint-disable-line import/no-unresolved
import request from 'request-promise';
import B from 'bluebird';
import _ from 'lodash';
import { copyGradleProjectRecursively } from './utils';


const TEST_APK_PATH = path.resolve(__dirname, '..', '..', 'espresso-server', 'app', 'build', 'outputs', 'apk', 'androidTest', 'debug', 'app-debug-androidTest.apk');
const TEST_MANIFEST_PATH = path.resolve(__dirname, '..', '..', 'espresso-server', 'AndroidManifest-test.xml');
const TEST_SERVER_ROOT = path.resolve(__dirname, '..', '..', 'espresso-server');
const TEST_MANIFEST_PATH = path.resolve(TEST_SERVER_ROOT, 'AndroidManifest-test.xml');
const TEST_APK_PKG = 'io.appium.espressoserver.test';
const REQUIRED_PARAMS = ['adb', 'tmpDir', 'host', 'systemPort', 'devicePort', 'appPackage', 'forceEspressoRebuild'];
const ESPRESSO_SERVER_LAUNCH_TIMEOUT = 30000;
Expand All @@ -28,6 +30,8 @@ class EspressoRunner {
this.proxyReqRes = this.jwproxy.proxyReqRes.bind(this.jwproxy);

this.modServerPath = path.resolve(this.tmpDir, `${TEST_APK_PKG}_${version}_${this.appPackage}.apk`);
this.showGradleLog = opts.showGradleLog;
this.espressoBuildConfig = opts.espressoBuildConfig;

this.serverLaunchTimeout = opts.serverLaunchTimeout || ESPRESSO_SERVER_LAUNCH_TIMEOUT;
this.androidInstallTimeout = opts.androidInstallTimeout;
Expand Down Expand Up @@ -107,6 +111,26 @@ class EspressoRunner {
}

async buildNewModServer () {
let buildConfiguration = {};
if (this.espressoBuildConfig) {
logger.info(`Using build configuration JSON from: '${this.espressoBuildConfig}'`);
try {
buildConfiguration = JSON.parse(await fs.readFile(this.espressoBuildConfig, 'utf8'));
} catch (e) {
logger.error('Failed to parse build configuration JSON', e);
throw e;
}
}
logger.info('Building espresso server');
const serverPath = path.resolve(this.tmpDir, 'espresso-server');
logger.debug(`build dir: ${serverPath}`);
await fs.rimraf(serverPath);
await mkdirp(serverPath);
logger.debug(`Copying espresso server template from (${TEST_SERVER_ROOT} to ${serverPath})`);
await copyGradleProjectRecursively(TEST_SERVER_ROOT, serverPath);
await new ServerBuilder({serverPath, buildConfiguration, showGradleLog: this.showGradleLog}).build();
const apkPath = path.resolve(serverPath, 'app', 'build', 'outputs', 'apk', 'androidTest', 'debug', 'app-debug-androidTest.apk');

logger.info(`Repackaging espresso server for: '${this.appPackage}'`);
const packageTmpDir = path.resolve(this.tmpDir, this.appPackage);
const newManifestPath = path.resolve(this.tmpDir, 'AndroidManifest.xml');
Expand All @@ -116,7 +140,7 @@ class EspressoRunner {
await mkdirp(packageTmpDir);
await fs.copyFile(TEST_MANIFEST_PATH, newManifestPath);
await this.adb.compileManifest(newManifestPath, TEST_APK_PKG, this.appPackage); // creates a file `${newManifestPath}.apk`
await this.adb.insertManifest(newManifestPath, TEST_APK_PATH, this.modServerPath); // copies from second to third and add manifest
await this.adb.insertManifest(newManifestPath, apkPath, this.modServerPath); // copies from second to third and add manifest
logger.info(`Repackaged espresso server ready: '${this.modServerPath}'`);
}

Expand Down
158 changes: 158 additions & 0 deletions lib/server-builder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import { SubProcess } from 'teen_process';
import { fs, logger, system } from 'appium-support';
import _ from 'lodash';
import log from './logger';
import path from 'path';
import { EOL } from 'os';

const GRADLE_VERSION_KEY = 'gradle';
const GRADLE_URL_PREFIX = 'distributionUrl=';
const GRADLE_URL_TEMPLATE = 'https\\://services.gradle.org/distributions/gradle-VERSION-all.zip';
const GRADLE_MAX_ERROR_LOG_LINES = 15;

const GRADLE_DEPENDENCIES_PLACEHOLDER = '// additionalAppDependencies placeholder (don\'t change or delete this line)';

const VERSION_KEYS = [
GRADLE_VERSION_KEY,
'androidGradlePlugin',
'compileSdk',
'buildTools',
'minSdk',
'targetSdk',
'kotlin'
];

const gradleLog = logger.getLogger('Gradle');

class ServerBuilder {
constructor (args = {}) {
this.serverPath = args.serverPath;
this.showGradleLog = args.showGradleLog;

const buildConfiguration = args.buildConfiguration || {};

const versionConfiguration = buildConfiguration.toolsVersions || {};
this.serverVersions = _.reduce(versionConfiguration, (acc, value, key) => {
if (VERSION_KEYS.includes(key)) {
acc[key] = value;
} else {
log.warn(`Got unexpected '${key}' in toolsVersion block of the build configuration`);
}
return acc;
}, {});

this.additionalAppDependencies = buildConfiguration.additionalAppDependencies || [];
}

async build () {
if (this.serverVersions[GRADLE_VERSION_KEY]) {
await this.setGradleWrapperVersion(this.serverVersions[GRADLE_VERSION_KEY]);
}

if (!Array.isArray(this.additionalAppDependencies)) {
throw new Error('additionalAppDependencies must be an array');
}
if (!_.isEmpty(this.additionalAppDependencies)) {
await this.insertAdditionalAppDependencies(this.additionalAppDependencies);
}

await this.runBuildProcess();
}

getCommand () {
const cmd = system.isWindows() ? 'gradlew.bat' : './gradlew';
let args = VERSION_KEYS
.filter(key => key !== GRADLE_VERSION_KEY)
.map(key => {
const serverVersion = this.serverVersions[key];
const gradleProperty = `appium${key.charAt(0).toUpperCase()}${key.slice(1)}`;
return serverVersion ? `-P${gradleProperty}=${serverVersion}` : null;
})
.filter(Boolean);

args.push('assembleAndroidTest');

return {cmd, args};
}

async setGradleWrapperVersion (version) {
const propertiesPath = path.resolve(this.serverPath, 'gradle', 'wrapper', 'gradle-wrapper.properties');
const originalProperties = await fs.readFile(propertiesPath, 'utf8');
const newProperties = this.updateGradleDistUrl(originalProperties, version);
await fs.writeFile(propertiesPath, newProperties, 'utf8');
}

updateGradleDistUrl (propertiesContent, version) {
return propertiesContent.replace(
new RegExp(`^(${_.escapeRegExp(GRADLE_URL_PREFIX)}).+$`, 'gm'),
`$1${GRADLE_URL_TEMPLATE.replace('VERSION', version)}`
);
}

async insertAdditionalAppDependencies (additionalAppDependencies) {
const buildPath = path.resolve(this.serverPath, 'app', 'build.gradle');
const originalConfiguration = await fs.readFile(buildPath, 'utf8');
const newConfiguration = this.updateDependencyLines(originalConfiguration, additionalAppDependencies);
await fs.writeFile(buildPath, newConfiguration, 'utf8');
}

updateDependencyLines (configurationContent, additionalAppDependencies) {
const dependencyLines = additionalAppDependencies
.map(function (dependency) {
// Disallow whitespace and quote characters that can break string literals in a patched build.gradle
// and dollar characters that otherwise would be interpreted by String.replace below
if (/[\s'\\$]/.test(dependency)) {
throw new Error('Single quotes, dollar characters and whitespace characters' +
` are disallowed in additional dependencies: ${dependency}`);
}
return `implementation '${dependency}'`;
})
.join('$1'); // interpreted as Group 1 from the pattern below

return configurationContent.replace(
// Group 1 captures new line characters and indentation (eg. '\n\t') used in build.gradle file
// This ensures that a patched build.gradle will have correct new lines and indentation
new RegExp(`(\\s*^\\s*)${_.escapeRegExp(GRADLE_DEPENDENCIES_PLACEHOLDER)}\\s*$`, 'gm'),
`$1${dependencyLines}`
);
}

async runBuildProcess () {
const {cmd, args} = this.getCommand();
log.debug(`Beginning build with command '${cmd} ${args.join(' ')}' ` +
`in directory '${this.serverPath}'`);
const gradlebuild = new SubProcess(cmd, args, {
cwd: this.serverPath,
stdio: ['ignore', 'pipe', 'pipe'],
});
let buildLastLines = [];

const logMsg = `Output from Gradle ${this.showGradleLog ? 'will' : 'will not'} be logged`;
log.debug(`${logMsg}. To change this, use 'showGradleLog' desired capability`);
gradlebuild.on('stream-line', line => {
if (this.showGradleLog) {
if (line.startsWith('[STDERR]')) {
gradleLog.warn(line);
} else {
gradleLog.info(line);
}
}
buildLastLines.push(`${EOL}${line}`);
if (buildLastLines.length > GRADLE_MAX_ERROR_LOG_LINES) {
buildLastLines = buildLastLines.slice(-GRADLE_MAX_ERROR_LOG_LINES);
}
});

try {
await gradlebuild.start();
await gradlebuild.join();
} catch (err) {
let msg = `Unable to build Espresso server - ${err.message}\n` +
`Gradle error message:${EOL}${buildLastLines}`;
log.errorAndThrow(msg);
}
}
}

export { ServerBuilder, VERSION_KEYS, GRADLE_URL_TEMPLATE, GRADLE_DEPENDENCIES_PLACEHOLDER };
export default ServerBuilder;
28 changes: 27 additions & 1 deletion lib/utils.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { fs, mkdirp } from 'appium-support';
import _ from 'lodash';
import path from 'path';

/**
* https://android.googlesource.com/platform/frameworks/base/+/master/tools/aapt/Resource.cpp#755
Expand All @@ -22,4 +24,28 @@ function qualifyActivityName (activityName, packageName) {
return `${packageName}${dotPos === 0 ? '' : '.'}${activityName}`;
}

export { qualifyActivityName };
/**
* Recursively copy all files except build directories contents
* @param sourceBaseDir {string} directory to copy files from
* @param targetBaseDir {string} directory to copy files to
*/
async function copyGradleProjectRecursively (sourceBaseDir, targetBaseDir) {
await fs.walkDir(sourceBaseDir, true, async (itemPath, isDirectory) => {
const relativePath = path.relative(sourceBaseDir, itemPath);
const targetPath = path.resolve(targetBaseDir, relativePath);

const isInGradleBuildDir = `${path.sep}${itemPath}`.includes(`${path.sep}build${path.sep}`);
if (isInGradleBuildDir) {
return false;
}

if (isDirectory) {
await mkdirp(targetPath);
} else {
await fs.copyFile(itemPath, targetPath);
}
return false;
});
}

export { qualifyActivityName, copyGradleProjectRecursively };
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,7 @@
"lib",
"build/index.js",
"build/lib",
"espresso-server/app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk",
"espresso-server/AndroidManifest-test.xml"
"espresso-server"
],
"dependencies": {
"@babel/runtime": "^7.4.3",
Expand Down
Loading

0 comments on commit a402a53

Please sign in to comment.