From 4a5669e533e4115ae21d5387ba04a039a6e4a0a0 Mon Sep 17 00:00:00 2001 From: Bao Nguyen Date: Mon, 24 Aug 2026 17:26:41 +0700 Subject: [PATCH 1/2] [eas-cli] Fix flavor detection when build.gradle uses string interpolation gradle-to-js counts braces without knowing about string literals, so the braces of a Groovy interpolation containing a method call, such as buildConfigField "String", "KEY", "\"${System.getenv("KEY")}\"", make it swallow the closing brace of the surrounding block. flavorDimensions and productFlavors then end up nested inside android.defaultConfig, and parseGradleCommand fails with "flavor staging is not defined". Unwrap string interpolations before parsing, keeping their content and dropping only the braces that confuse the parser. --- .../string-interpolation-in-build.gradle | 62 +++++++++++++++++++ .../android/__tests__/gradleUtils-test.ts | 29 +++++++++ .../src/project/android/gradleUtils.ts | 23 ++++++- 3 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 packages/eas-cli/src/project/android/__tests__/fixtures/string-interpolation-in-build.gradle diff --git a/packages/eas-cli/src/project/android/__tests__/fixtures/string-interpolation-in-build.gradle b/packages/eas-cli/src/project/android/__tests__/fixtures/string-interpolation-in-build.gradle new file mode 100644 index 0000000000..1a8a9b8b6a --- /dev/null +++ b/packages/eas-cli/src/project/android/__tests__/fixtures/string-interpolation-in-build.gradle @@ -0,0 +1,62 @@ +apply plugin: "com.android.application" + +import com.android.build.OutputFile + +def enableProguardInReleaseBuilds = false +def jscFlavor = 'org.webkit:android-jsc:+' + +android { + compileSdkVersion rootProject.ext.compileSdkVersion + + buildFeatures { + buildConfig true + } + + defaultConfig { + applicationId "com.testapp" + minSdkVersion rootProject.ext.minSdkVersion + targetSdkVersion rootProject.ext.targetSdkVersion + versionCode 1 + versionName "1.0" + + buildConfigField "String", "API_KEY", "\"${System.getenv("API_KEY")}\"" + buildConfigField "String", "BUILD_LABEL", "\"${System.getenv("FLAVOR")}-${System.getenv("STAGE")}\"" + resValue "string", "app_id", "\"${System.getenv("APP_ID")}\"" + } + flavorDimensions "env" + productFlavors { + staging { + dimension "env" + applicationId "com.testapp.staging" + versionCode 123 + } + production { + dimension "env" + applicationId "com.testapp" + versionCode 124 + } + } + buildTypes { + release { + minifyEnabled enableProguardInReleaseBuilds + proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" + } + } + + applicationVariants.all { variant -> + variant.outputs.each { output -> + def abi = output.getFilter(OutputFile.ABI) + if (abi != null) { + output.versionCodeOverride = defaultConfig.versionCode + } + } + } +} + +dependencies { + implementation "com.facebook.react:react-native:+" + debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}") { + exclude group:'com.facebook.fbjni' + } + implementation jscFlavor +} diff --git a/packages/eas-cli/src/project/android/__tests__/gradleUtils-test.ts b/packages/eas-cli/src/project/android/__tests__/gradleUtils-test.ts index ffcf073902..c235db823b 100644 --- a/packages/eas-cli/src/project/android/__tests__/gradleUtils-test.ts +++ b/packages/eas-cli/src/project/android/__tests__/gradleUtils-test.ts @@ -166,6 +166,35 @@ describe(getAppBuildGradleAsync, () => { }, }); }); + + test('parsing build.gradle with interpolated strings', async () => { + vol.fromJSON( + { + 'android/app/build.gradle': await fsReal.promises.readFile( + path.join(__dirname, 'fixtures/string-interpolation-in-build.gradle'), + 'utf-8' + ), + }, + '/test' + ); + const buildGradle = await getAppBuildGradleAsync('/test'); + expect(pick(buildGradle?.android ?? {}, ['flavorDimensions', 'productFlavors'])).toEqual({ + flavorDimensions: 'env', + productFlavors: { + staging: { + applicationId: 'com.testapp.staging', + versionCode: '123', + dimension: 'env', + }, + production: { + applicationId: 'com.testapp', + versionCode: '124', + dimension: 'env', + }, + }, + }); + expect(buildGradle?.android?.defaultConfig?.applicationId).toBe('com.testapp'); + }); }); describe(parseGradleCommand, () => { diff --git a/packages/eas-cli/src/project/android/gradleUtils.ts b/packages/eas-cli/src/project/android/gradleUtils.ts index 70e8128c88..a54f4bde4e 100644 --- a/packages/eas-cli/src/project/android/gradleUtils.ts +++ b/packages/eas-cli/src/project/android/gradleUtils.ts @@ -45,7 +45,28 @@ export async function getAppBuildGradleAsync(projectDir: string): Promise !line.trim().startsWith('//')) .join('\n'); - return await g2js.parseText(rawBuildGradleWithoutComments); + return await g2js.parseText(unwrapStringInterpolations(rawBuildGradleWithoutComments)); +} + +/** + * gradle-to-js counts `{` and `}` without knowing about string literals, so the braces of a + * Groovy string interpolation are treated as a block. When the interpolation contains a method + * call, like `buildConfigField "String", "KEY", "\"${System.getenv("KEY")}\""`, the parser + * skips one character too many and swallows the closing brace of the surrounding block. Every + * entry that follows then ends up nested in the wrong place, which is why `android.productFlavors` + * comes back as `undefined` for projects that use interpolated build config fields. + * + * Unwrapping the interpolations drops the braces and keeps their content. `[^{}]*` never matches + * across a brace, so nested interpolations are unwrapped one level per pass. + */ +function unwrapStringInterpolations(buildGradle: string): string { + let result = buildGradle; + let previousResult; + do { + previousResult = result; + result = result.replace(/\$\{([^{}]*)\}/g, '$$$1'); + } while (result !== previousResult); + return result; } export function resolveConfigValue( From e7d66b8c4bba21f44b61d9e1d6a151efd174c3e0 Mon Sep 17 00:00:00 2001 From: Bao Nguyen Date: Mon, 24 Aug 2026 17:27:56 +0700 Subject: [PATCH 2/2] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d46408f8ab..6913551aa8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ This is the log of notable changes to EAS CLI and related packages. - [build-tools] Reduce `expo-device-hub` preview resolution from 1280 px to 960 px to match `serve-sim` and lower streaming bandwidth. ([#4326](https://github.com/expo/eas-cli/pull/4326) by [@krystofwoldrich-agent](https://github.com/krystofwoldrich-agent)) - [build-tools] Install FFmpeg before launching `expo-device-hub` in Android device sessions. ([#4332](https://github.com/expo/eas-cli/pull/4332) by [@krystofwoldrich-agent](https://github.com/krystofwoldrich-agent)) +- [eas-cli] Fix flavor detection in `build.gradle` files that use string interpolation, such as `buildConfigField "String", "KEY", "\"${System.getenv("KEY")}\""`, which made `eas build` fail with "Failed to autodetect applicationId in multi-flavor project". ([#4260](https://github.com/expo/eas-cli/pull/4260) by [@giaBaoJS](https://github.com/giaBaoJS)) ### 🧹 Chores