From 51ffa8d4638ff4389bb9672cdf4154412eee4395 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 5275cfb88a2fbc30cd23cd55d0536a19d0abbdb7 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 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a390c6fa1..22c6be1c58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ This is the log of notable changes to EAS CLI and related packages. ### ๐Ÿ› Bug fixes +- [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 ## [23.2.0](https://github.com/expo/eas-cli/releases/tag/v23.2.0) - 2026-08-31