Skip to content

Nested project discovery (auto-enabled for Flutter-shaped roots) follows symlinks out of the workspace into ~/.pub-cache #1905

Description

Description

gradle.nestedProjects (default false) auto-enables recursive nested-project discovery whenever a workspace folder's root has no Gradle marker file — this is exactly the Flutter project shape (root has pubspec.yaml, the real Gradle root is android/). In that state, getNestedRootProjectFolders() runs an unscoped vscode.workspace.findFiles('**/{build.gradle,build.gradle.kts,settings.gradle,settings.gradle.kts}') across the whole workspace, with no exclude and no depth limit.

Because search.followSymlinks defaults to true, this glob follows Flutter's **/ephemeral/.plugin_symlinks/* symlinks straight into ~/.pub-cache, which is outside the workspace. Almost every pub.dev plugin ships a standalone example/android sample app with its own pinned gradle-wrapper.properties (independent of the real project's Gradle version), so each one gets picked up as an independent "root project" candidate. When the extension later tries to connect to these with the Gradle Tooling API, it uses each one's own (often ancient) wrapper version against the workspace's configured JDK, producing errors like:

Could not use Gradle version 8.3 and Java version 21 to build project
Could not use Gradle version 7.5 and Java version 21 to build project

Flutter also creates .plugin_symlinks for every platform declared in the project (ios/, macos/, linux/, windows/), even on machines where only Android/macOS are actually used, and each duplicate symlink tree gets walked separately, so the fan-out into the pub-cache can reach thousands of spurious projects. Reproduced locally against one real-world plugin:

$ cat ~/.pub-cache/hosted/pub.dev/file_picker-8.3.7/example/android/gradle/wrapper/gradle-wrapper.properties
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip

Two things I believe are worth fixing

  1. README/behavior mismatch. The README states "Nested Gradle project discovery is not enabled by default, set gradle.nestedProjects: true to enable it." But RootProjectsStore.populate() actually does:

    const shouldDiscoverNestedProjects =
        configNestedFolders === true ||
        (!Array.isArray(configNestedFolders) && !hasRootGradleMarker);

    So discovery is silently on by default whenever the workspace folder root itself has no build.gradle/settings.gradle — which is precisely the Flutter case this feature was built for in Add support for nested gradle projects #88. The README should say this explicitly, since users (like me) spend a long time trying gradle.nestedProjects: false / leaving it unset, not realizing it's already active.

  2. findFiles() is unscoped and follows symlinks out of the workspace. getNestedRootProjectFolders() calls vscode.workspace.findFiles(pattern) with no exclude argument and no maxResults. Per the VS Code API doc for findFiles, when exclude is undefined only files.exclude is honored — not search.exclude — which is surprising and not mentioned anywhere in this extension's own docs. Combined with search.followSymlinks defaulting to true, this means the scan can walk arbitrarily far outside the workspace through any symlink (Flutter's ephemeral/.plugin_symlinks, but this would equally affect any other symlink-heavy toolchain), with no way for users to stop it via the settings they'd naturally try (search.exclude, java.import.exclusions — the latter belongs to a different extension entirely).

Suggested fixes (any of these would help; happy to send a PR)

  • Document in the README that discovery auto-enables per-folder when there's no root Gradle marker (not just via explicit true).
  • Pass an explicit exclude to findFiles() (or intersect with search.exclude) so users have a working way to fence off the scan without switching to the array form.
  • Consider not following symlinks in the nested-discovery scan by default (or cap recursion at workspace-folder boundaries), since a discovery heuristic silently reaching into arbitrary locations outside the workspace (here, the pub-cache) seems more surprising than useful.

Workaround (works, for anyone finding this)

Use the array form of gradle.nestedProjects (e.g. "gradle.nestedProjects": ["android"]) instead of leaving it unset/false. This skips getNestedRootProjectFolders() entirely (the array branch returns early), so no symlinks are ever walked. Note the setting is "scope": "window", so in a multi-root workspace it must go in the .code-workspace file's top-level settings, not in an individual folder's .vscode/settings.json.

Environment

  • vscode-gradle: latest (checked against main as of this report)
  • OS: macOS (Apple Silicon)
  • VS Code multi-root workspace, one folder is a Flutter project (android/ holds the real Gradle build)
  • flutter --version: Flutter/Dart with a project depending on several pub.dev plugins (e.g. file_picker, package_info_plus, connectivity_plus)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions