Skip to content

[eas-cli] Fix flavor detection when build.gradle uses string interpolation - #4260

Open
giaBaoJS wants to merge 2 commits into
expo:mainfrom
giaBaoJS:fix-gradle-string-interpolation-parsing
Open

[eas-cli] Fix flavor detection when build.gradle uses string interpolation#4260
giaBaoJS wants to merge 2 commits into
expo:mainfrom
giaBaoJS:fix-gradle-string-interpolation-parsing

Conversation

@giaBaoJS

@giaBaoJS giaBaoJS commented Aug 24, 2026

Copy link
Copy Markdown

Why

Fixes #2815.

In a bare project, android/app/build.gradle lines like this one make EAS CLI stop seeing product flavors:

buildConfigField "String", "BRAZE_ANDROID_API_KEY", "\"${System.getenv("BRAZE_ANDROID_API_KEY")}\""

flavorDimensions and productFlavors end up nested inside android.defaultConfig instead of on android, so parseGradleCommand reads buildGradle.android.productFlavors as undefined and throws flavor staging is not defined. The user sees:

Unable to read gradle project config: flavor staging is not defined.
Values from app/build.gradle might be resolved incorrectly.
Error: Failed to autodetect applicationId in multi-flavor project.

The reporter found the workaround of switching to the parenthesised call form, but a normal space-separated Gradle call should work.

How

gradle-to-js counts { and } without knowing about string literals, so the braces of a Groovy interpolation are treated as a block. Concretely, in gradle-to-js@2.0.1:

  • deepParse sees the { of ${...} and recurses into it as if it were a closure.
  • Inside that recursion parsingKey is true, so the ( of the method call sends it into skipFunctionCall.
  • skipFunctionCall does state.index++ before it breaks, then deepParse continues and the for loop increments state.index again. One character past the closing ) is swallowed, and in ${System.getenv("KEY")} that character is the closing }.

The recursion therefore never ends where it should and eats the closing brace of defaultConfig instead. Everything after that line stays nested one level too deep.

I narrowed the trigger down to braces that contain parentheses. "${abc}" parses fine today; "${a()}" is enough to break it. It is not specific to buildConfigField, resValue breaks identically.

The fix preprocesses the file before handing it to gradle-to-js, in the same spirit as the existing comment-stripping right above it (and as #2435, which added the same kind of workaround for empty single-line comments). Interpolations are unwrapped: the braces go, the content stays.

result = result.replace(/\$\{([^{}]*)\}/g, '$$$1');

Notes on the rule:

  • [^{}]* cannot match across a brace, so a Groovy closure inside an interpolation, like "${list.collect{ it }.join()}", is left alone. That case already parses correctly today and still does.
  • Because of that, nested interpolations are unwrapped one level per pass, so the replace runs to a fixed point. Each pass removes at least one brace pair, so it terminates.
  • Multiple interpolations on one line are handled by the g flag.
  • A $ with no braces, $ in a plain string, and top-level closures such as doLast { ... } are never matched, since the pattern requires the literal ${.
  • ${ has no meaning in Groovy other than interpolation, so this does not touch anything else.

Test Plan

New test parsing build.gradle with interpolated strings with fixture string-interpolation-in-build.gradle, modelled on the build.gradle in the issue. It covers the reported line, two interpolations on one line, resValue, and an interpolation followed by a real closure (debugImplementation("...:${FLIPPER_VERSION}") { exclude ... }).

Passing on this branch:

$ yarn jest src/project/android/__tests__/gradleUtils-test.ts
PASS src/project/android/__tests__/gradleUtils-test.ts
  getAppBuildGradleAsync
    ✓ parsing build.gradle from managed template (8 ms)
    ✓ parsing build gradle with empty single line comment (2 ms)
    ✓ parsing multiflavor build.gradle (2 ms)
    ✓ parsing build.gradle with flavor dimensions (4 ms)
    ✓ parsing build.gradle with flavor dimensions as array (2 ms)
    ✓ parsing build.gradle with interpolated strings (1 ms)
  parseGradleCommand
    ✓ parsing :app:bundleRelease
    ...
Tests:       18 passed, 18 total

Reverting only the change in gradleUtils.ts and keeping the test and fixture makes it fail for the right reason, with both keys missing from android:

  ● getAppBuildGradleAsync › parsing build.gradle with interpolated strings

    - Expected  - 15
    + Received  +  1

    - Object {
    -   "flavorDimensions": "env",
    -   "productFlavors": Object {
    -     "production": Object { "applicationId": "com.testapp", "dimension": "env", "versionCode": "124" },
    -     "staging": Object { "applicationId": "com.testapp.staging", "dimension": "env", "versionCode": "123" },
    -   },
    - }
    + Object {}

For the same fixture, the parse result before and after the fix:

before after
android.flavorDimensions undefined "env"
android.productFlavors undefined { staging, production }
android.defaultConfig keys applicationId, minSdkVersion, targetSdkVersion, versionCode, versionName, buildConfigField (with resValue, flavorDimensions, productFlavors, buildTypes and dependencies misnested inside buildConfigField) applicationId, minSdkVersion, targetSdkVersion, versionCode, versionName, buildConfigField, resValue

No behaviour change on anything that parses today. All five pre-existing fixtures produce byte-identical JSON before and after, and the preprocessing does rewrite text in every one of them (they all contain ${FLIPPER_VERSION}), so the comparison is not vacuous:

IDENTICAL  build.gradle                                   (text rewritten: true)
IDENTICAL  empty-single-line-comment-in-build.gradle      (text rewritten: true)
IDENTICAL  multiflavor-build.gradle                       (text rewritten: true)
IDENTICAL  multiflavor-with-dimensions-build.gradle       (text rewritten: true)
IDENTICAL  multiflavor-with-dimensions-array-build.gradle (text rewritten: true)

I also drove gradle-to-js@2.0.1 directly over a matrix of Groovy constructs. Everything that parsed correctly before still does, and these now parse correctly too: ${System.getenv("K")}, ${a()}, ${a}-${b} with calls, resValue with an interpolation, single-quoted interpolations, an interpolation inside a closure, and nested interpolations such as ${a("${b()}")}. Untouched and still correct: doLast { ... }, applicationVariants.all { variant -> ... }, "1.0-$suffix", "$9.99", manifestPlaceholders = [...], and "${list.collect{ it }.join()}".

Whole eas-cli package suite: 2439 passed, with the only 2 failing suites (src/observe/__tests__/formatEvents.test.ts and formatCustomEvents.test.ts) failing identically on a clean main on my machine. They are date-locale snapshots (Jan 1, 2025 vs 1 Jan 2025), unrelated to this change.

yarn typecheck, yarn lint (0 warnings, 0 errors) and yarn fmt:check all pass.

@github-actions

Copy link
Copy Markdown

Subscribed to pull request

File Patterns Mentions
packages/eas-cli/** @douglowder

Generated by CodeMention

Warning: The preamble and epilogue options in commentConfiguration are deprecated. Use template instead.

…ation

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.
@giaBaoJS
giaBaoJS force-pushed the fix-gradle-string-interpolation-parsing branch from 8a2bea9 to 3f650d2 Compare August 25, 2026 02:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

buildConfigField seems to bug gradle parser causing flavor to not be detected

1 participant