UNPKG

appium-espresso-driver

Version:
218 lines 9.24 kB
import { SubProcess } from 'teen_process'; import { fs, system } from 'appium/support.js'; import path from 'node:path'; import { EOL } from 'node:os'; import { escapeRegExp } from '../../utils/index.js'; const GRADLE_VERSION_KEY = 'gradle'; const GRADLE_URL_PREFIX = 'distributionUrl='; export const GRADLE_URL_TEMPLATE = 'https\\://services.gradle.org/distributions/gradle-VERSION-all.zip'; const DEPENDENCY_PROP_NAMES = [ 'additionalAppDependencies', 'additionalAndroidTestDependencies', ]; export const VERSION_KEYS = [ GRADLE_VERSION_KEY, 'androidGradlePlugin', 'compileSdk', 'buildTools', 'minSdk', 'targetSdk', 'kotlin', 'sourceCompatibility', 'targetCompatibility', 'jvmTarget', 'composeVersion', 'espressoVersion', 'annotationVersion', ]; export class ServerBuilder { log; serverPath; showGradleLog; serverVersions; testAppPackage; signingConfig; additionalAppDependencies = []; additionalAndroidTestDependencies = []; composeSupport; constructor(log, args) { this.log = log; this.serverPath = args.serverPath; this.showGradleLog = args.showGradleLog; const buildConfiguration = args.buildConfiguration || {}; this.composeSupport = buildConfiguration.composeSupport !== false; const versionConfiguration = buildConfiguration.toolsVersions || {}; this.serverVersions = {}; for (const [key, value] of Object.entries(versionConfiguration)) { if (VERSION_KEYS.includes(key)) { this.serverVersions[key] = value; } else { log.warn(`Got unexpected '${key}' in toolsVersion block of the build configuration`); } } this.testAppPackage = args.testAppPackage; this.signingConfig = args.signingConfig; for (const propName of DEPENDENCY_PROP_NAMES) { this[propName] = buildConfiguration[propName] || []; } } async build() { const gradleVersion = this.serverVersions[GRADLE_VERSION_KEY]; if (gradleVersion) { await this.setGradleWrapperVersion(gradleVersion); } await this.insertAdditionalDependencies(); await this.runBuildProcess(); } getCommand() { const cmd = system.isWindows() ? 'gradlew.bat' : path.resolve(this.serverPath, 'gradlew'); const buildProperty = (key, value) => value ? `-P${key}=${value}` : null; const 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 buildProperty(gradleProperty, serverVersion); }) .filter((arg) => typeof arg === 'string' && Boolean(arg)); const signingConfig = this.signingConfig; if (signingConfig) { args.push(...Object.keys(signingConfig) .map((key) => { const propKey = key; const propValue = signingConfig[propKey]; const k = `appium${key.charAt(0).toUpperCase()}${key.slice(1)}`; const v = propValue != null ? String(propValue) : undefined; return buildProperty(k, v); }) .filter((arg) => typeof arg === 'string' && Boolean(arg))); } if (this.testAppPackage) { const targetPackageArg = buildProperty('appiumTargetPackage', this.testAppPackage); if (targetPackageArg) { args.push(targetPackageArg); } } if (!this.composeSupport) { const composeArg = buildProperty('appiumComposeSupport', 'false'); if (composeArg) { args.push(composeArg); } } args.push('app: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 insertAdditionalDependencies() { let hasAdditionalDeps = false; for (const propName of DEPENDENCY_PROP_NAMES) { const deps = this[propName]; if (!Array.isArray(deps)) { throw new Error(`'${propName}' must be an array`); } if (deps.filter((line) => line.trim()).length === 0) { continue; } for (const dep of deps) { if (/[\s'\\$]/.test(dep)) { throw new Error('Single quotes, dollar characters and whitespace characters' + ` are disallowed in additional dependencies: ${dep}`); } } hasAdditionalDeps = true; } if (!hasAdditionalDeps) { return; } const buildPath = path.resolve(this.serverPath, 'app', 'build.gradle.kts'); let configuration = await fs.readFile(buildPath, 'utf8'); for (const propName of DEPENDENCY_PROP_NAMES) { const prefix = propName === DEPENDENCY_PROP_NAMES[0] ? 'api' : 'androidTestImplementation'; const deps = this[propName] .filter((line) => line.trim()) .map((line) => `${prefix}("${line}")`); if (deps.length === 0) { continue; } this.log.info(`Adding the following ${propName} to build.gradle.kts: ${deps}`); configuration = updateDependencyLines(configuration, propName, deps); } await fs.writeFile(buildPath, configuration, 'utf8'); } async runBuildProcess() { const { cmd, args } = this.getCommand(); this.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'], // https://github.com/nodejs/node/issues/52572 shell: system.isWindows(), windowsVerbatimArguments: true, }); const gradleError = []; const logMsg = `Output from Gradle ${this.showGradleLog ? 'will' : 'will not'} be logged`; this.log.debug(`${logMsg}. To change this, use 'showGradleLog' desired capability`); gradlebuild.on('line-stderr', (line) => { this.log.warn(`[Gradle] ${line}`); gradleError.push(line); }); gradlebuild.on('line-stdout', (line) => this.log.info(`[Gradle] ${line}`)); try { await gradlebuild.start(); await gradlebuild.join(); } catch (err) { const msg = `Unable to build Espresso server - ${err.message}\n` + `Gradle error message:${EOL}${gradleError.join('\n')}`; throw this.log.errorWithException(msg); } finally { gradlebuild.removeAllListeners(); } } } /** * Build keystore options for signing the built Espresso server test APK. * * @param args - Paths and credentials for the keystore used when signing. * @returns Configuration consumed by the Gradle signing step. */ export function buildServerSigningConfig(args) { return { zipAlign: true, keystoreFile: args.keystoreFile, keystorePassword: args.keystorePassword, keyAlias: args.keyAlias, keyPassword: args.keyPassword, }; } /** * Insert Gradle dependency lines after a `// placeholder` marker in a Gradle file. * * @param originalContent - Full text of the Gradle configuration file. * @param dependencyPlaceholder - Placeholder comment label to find (e.g. from build.gradle). * @param dependencyLines - Dependency lines to insert (e.g. `implementation "..."`). * @returns Updated file content, or the original string if the placeholder is missing. */ export function updateDependencyLines(originalContent, dependencyPlaceholder, dependencyLines) { const configurationLines = originalContent.split('\n'); const searchRe = new RegExp(`^\\s*//\\s*\\b${escapeRegExp(dependencyPlaceholder)}\\b`, 'm'); const placeholderIndex = configurationLines.findIndex((line) => searchRe.test(line)); if (placeholderIndex < 0) { return originalContent; } const placeholderLine = configurationLines[placeholderIndex]; const indentLen = placeholderLine.length - placeholderLine.trimStart().length; configurationLines.splice(placeholderIndex + 1, 0, ...dependencyLines.map((line) => `${' '.repeat(indentLen)}${line}`)); return configurationLines.join('\n'); } //# sourceMappingURL=builder.js.map