From f15ecd7c1041b44f6f955e5e1a32912c03abf0da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw=20Chmiela?= Date: Thu, 25 Jun 2026 13:52:00 +0200 Subject: [PATCH] Use exec when launching Gradle --- packages/build-tools/src/android/gradle.ts | 73 ++--------- .../src/steps/utils/android/gradle.ts | 113 +----------------- packages/build-tools/src/utils/gradle.ts | 95 +++++++++++++++ 3 files changed, 106 insertions(+), 175 deletions(-) create mode 100644 packages/build-tools/src/utils/gradle.ts diff --git a/packages/build-tools/src/android/gradle.ts b/packages/build-tools/src/android/gradle.ts index 9f616ad1c5..56aae99a35 100644 --- a/packages/build-tools/src/android/gradle.ts +++ b/packages/build-tools/src/android/gradle.ts @@ -1,12 +1,10 @@ import { Android, Env, Job, Platform } from '@expo/eas-build-job'; import { bunyan } from '@expo/logger'; -import spawn, { SpawnPromise, SpawnResult } from '@expo/turtle-spawn'; -import assert from 'assert'; import fs from 'fs-extra'; import path from 'path'; import { BuildContext } from '../context'; -import { getParentAndDescendantProcessPidsAsync } from '../utils/processes'; +import { runGradleCommand as runGradleCommandInternal } from '../utils/gradle'; export async function ensureLFLineEndingsInGradlewScript( ctx: BuildContext @@ -28,67 +26,16 @@ export async function runGradleCommand( extraEnv, }: { logger: bunyan; gradleCommand: string; androidDir: string; extraEnv?: Env } ): Promise { - logger.info(`Running 'gradlew ${gradleCommand}' in ${androidDir}`); - await fs.chmod(path.join(androidDir, 'gradlew'), 0o755); - const verboseFlag = ctx.env['EAS_VERBOSE'] === '1' ? '--info' : ''; - - const spawnPromise = spawn( - 'bash', - ['-c', `./gradlew ${gradleCommand} --profile ${verboseFlag}`], - { - cwd: androidDir, - logger, - lineTransformer: (line?: string) => { - if (!line || /^\.+$/.exec(line)) { - return null; - } else { - return line; - } - }, - env: { - ...ctx.env, - ...extraEnv, - ...resolveVersionOverridesEnvs(ctx), - LC_ALL: 'C.UTF-8', - }, - } - ); - if (ctx.env.EAS_BUILD_RUNNER === 'eas-build' && process.platform === 'linux') { - adjustOOMScore(spawnPromise, logger); - } - - await spawnPromise; -} - -/** - * OOM Killer sometimes kills worker server while build is exceeding memory limits. - * `oom_score_adj` is a value between -1000 and 1000 and it defaults to 0. - * It defines which process is more likely to get killed (higher value more likely). - * - * This function sets oom_score_adj for Gradle process and all its child processes. - */ -function adjustOOMScore(spawnPromise: SpawnPromise, logger: bunyan): void { - setTimeout( - async () => { - try { - assert(spawnPromise.child.pid); - const pids = await getParentAndDescendantProcessPidsAsync(spawnPromise.child.pid); - await Promise.all( - pids.map(async (pid: number) => { - // Value 800 is just a guess here. It's probably higher than most other - // process. I didn't want to set it any higher, because I'm not sure if OOM Killer - // can start killing processes when there is still enough memory left. - const oomScoreOverride = 800; - await fs.writeFile(`/proc/${pid}/oom_score_adj`, `${oomScoreOverride}\n`); - }) - ); - } catch (err: any) { - logger.debug({ err, stderr: err?.stderr }, 'Failed to override oom_score_adj'); - } + await runGradleCommandInternal({ + androidDir, + env: { + ...ctx.env, + ...extraEnv, + ...resolveVersionOverridesEnvs(ctx), }, - // Wait 20 seconds to make sure all child processes are started - 20000 - ); + gradleCommand, + logger, + }); } // Version envs should be set at the beginning of the build, but when building diff --git a/packages/build-tools/src/steps/utils/android/gradle.ts b/packages/build-tools/src/steps/utils/android/gradle.ts index e9e1754d3c..c07d603507 100644 --- a/packages/build-tools/src/steps/utils/android/gradle.ts +++ b/packages/build-tools/src/steps/utils/android/gradle.ts @@ -1,117 +1,6 @@ import { Android } from '@expo/eas-build-job'; -import { bunyan } from '@expo/logger'; -import { BuildStepEnv } from '@expo/steps'; -import spawn, { SpawnPromise, SpawnResult } from '@expo/turtle-spawn'; -import assert from 'assert'; -import fs from 'fs-extra'; -import path from 'path'; -export async function runGradleCommand({ - logger, - gradleCommand, - androidDir, - env, - extraEnv, -}: { - logger: bunyan; - gradleCommand: string; - androidDir: string; - env: BuildStepEnv; - extraEnv?: BuildStepEnv; -}): Promise { - const verboseFlag = env['EAS_VERBOSE'] === '1' ? '--info' : ''; - - logger.info(`Running 'gradlew ${gradleCommand} ${verboseFlag}' in ${androidDir}`); - await fs.chmod(path.join(androidDir, 'gradlew'), 0o755); - - const spawnPromise = spawn( - 'bash', - ['-c', `./gradlew ${gradleCommand} --profile ${verboseFlag}`], - { - cwd: androidDir, - logger, - lineTransformer: (line?: string) => { - if (!line || /^\.+$/.exec(line)) { - return null; - } else { - return line; - } - }, - env: { - ...env, - ...extraEnv, - LC_ALL: 'C.UTF-8', - }, - } - ); - if (env.EAS_BUILD_RUNNER === 'eas-build' && process.platform === 'linux') { - adjustOOMScore(spawnPromise, logger); - } - - await spawnPromise; -} - -/** - * OOM Killer sometimes kills worker server while build is exceeding memory limits. - * `oom_score_adj` is a value between -1000 and 1000 and it defaults to 0. - * It defines which process is more likely to get killed (higher value more likely). - * - * This function sets oom_score_adj for Gradle process and all its child processes. - */ -function adjustOOMScore(spawnPromise: SpawnPromise, logger: bunyan): void { - setTimeout( - async () => { - try { - assert(spawnPromise.child.pid); - const pids = await getParentAndDescendantProcessPidsAsync(spawnPromise.child.pid); - await Promise.all( - pids.map(async (pid: number) => { - // Value 800 is just a guess here. It's probably higher than most other - // process. I didn't want to set it any higher, because I'm not sure if OOM Killer - // can start killing processes when there is still enough memory left. - const oomScoreOverride = 800; - await fs.writeFile(`/proc/${pid}/oom_score_adj`, `${oomScoreOverride}\n`); - }) - ); - } catch (err: any) { - logger.debug({ err, stderr: err?.stderr }, 'Failed to override oom_score_adj'); - } - }, - // Wait 20 seconds to make sure all child processes are started - 20000 - ); -} - -async function getChildrenPidsAsync(parentPids: number[]): Promise { - try { - const result = await spawn('pgrep', ['-P', parentPids.join(',')], { - stdio: 'pipe', - }); - return result.stdout - .toString() - .split('\n') - .map(i => Number(i.trim())) - .filter(i => i); - } catch { - return []; - } -} - -async function getParentAndDescendantProcessPidsAsync(ppid: number): Promise { - const children = new Set([ppid]); - let shouldCheckAgain = true; - while (shouldCheckAgain) { - const pids = await getChildrenPidsAsync([...children]); - shouldCheckAgain = false; - for (const pid of pids) { - if (!children.has(pid)) { - shouldCheckAgain = true; - children.add(pid); - } - } - } - return [...children]; -} +export { runGradleCommand } from '../../../utils/gradle'; export function resolveGradleCommand(job: Android.Job, command?: string): string { if (command) { diff --git a/packages/build-tools/src/utils/gradle.ts b/packages/build-tools/src/utils/gradle.ts new file mode 100644 index 0000000000..b7f9ed4987 --- /dev/null +++ b/packages/build-tools/src/utils/gradle.ts @@ -0,0 +1,95 @@ +import { bunyan } from '@expo/logger'; +import spawn, { SpawnPromise, SpawnResult } from '@expo/turtle-spawn'; +import assert from 'assert'; +import fs from 'fs-extra'; +import path from 'path'; + +import { getParentAndDescendantProcessPidsAsync } from './processes'; + +type GradleEnv = Record; + +export async function runGradleCommand({ + logger, + gradleCommand, + androidDir, + env, + extraEnv, +}: { + logger: bunyan; + gradleCommand: string; + androidDir: string; + env: GradleEnv; + extraEnv?: GradleEnv; +}): Promise { + const verboseFlag = env['EAS_VERBOSE'] === '1' ? '--info' : ''; + + logger.info(`Running 'gradlew ${gradleCommand} ${verboseFlag}' in ${androidDir}`); + await fs.chmod(path.join(androidDir, 'gradlew'), 0o755); + + const spawnPromise = spawn( + 'bash', + ['-c', getGradleShellCommand({ gradleCommand, verboseFlag })], + { + cwd: androidDir, + logger, + lineTransformer: (line?: string) => { + if (!line || /^\.+$/.exec(line)) { + return null; + } else { + return line; + } + }, + env: { + ...env, + ...extraEnv, + LC_ALL: 'C.UTF-8', + }, + } + ); + if (env.EAS_BUILD_RUNNER === 'eas-build' && process.platform === 'linux') { + adjustOOMScore(spawnPromise, logger); + } + + await spawnPromise; +} + +export function getGradleShellCommand({ + gradleCommand, + verboseFlag, +}: { + gradleCommand: string; + verboseFlag: string; +}): string { + return `exec ./gradlew ${gradleCommand} --profile ${verboseFlag}`; +} + +/** + * OOM Killer sometimes kills worker server while build is exceeding memory limits. + * `oom_score_adj` is a value between -1000 and 1000 and it defaults to 0. + * It defines which process is more likely to get killed (higher value more likely). + * + * This function sets oom_score_adj for Gradle process and all its child processes. + */ +function adjustOOMScore(spawnPromise: SpawnPromise, logger: bunyan): void { + setTimeout( + async () => { + try { + assert(spawnPromise.child.pid); + const pids = await getParentAndDescendantProcessPidsAsync(spawnPromise.child.pid); + await Promise.all( + pids.map(async (pid: number) => { + // Value 800 is just a guess here. It's probably higher than most other + // process. I didn't want to set it any higher, because I'm not sure if OOM Killer + // can start killing processes when there is still enough memory left. + const oomScoreOverride = 800; + await fs.writeFile(`/proc/${pid}/oom_score_adj`, `${oomScoreOverride}\n`); + }) + ); + } catch (err: any) { + logger.debug({ err, stderr: err?.stderr }, 'Failed to override oom_score_adj'); + } + }, + // Wait 20 seconds to make sure all child processes are started + 20000 + ); +}