Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
29 changes: 29 additions & 0 deletions packages/eas-cli/src/project/android/__tests__/gradleUtils-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, () => {
Expand Down
23 changes: 22 additions & 1 deletion packages/eas-cli/src/project/android/gradleUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,28 @@ export async function getAppBuildGradleAsync(projectDir: string): Promise<AppBui
.filter(line => !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(
Expand Down
Loading