Skip to content

fix(tvos_plugins): preserve existing .flutter-plugins-dependencies#15

Merged
DenisovAV merged 2 commits into
fluttertv:mainfrom
MAUstaoglu:fix/preserve-flutter-plugins-dependencies
May 26, 2026
Merged

fix(tvos_plugins): preserve existing .flutter-plugins-dependencies#15
DenisovAV merged 2 commits into
fluttertv:mainfrom
MAUstaoglu:fix/preserve-flutter-plugins-dependencies

Conversation

@MAUstaoglu
Copy link
Copy Markdown
Member

Summary

Fixes a regression in ensureReadyForTvosTooling where the CLI was wiping .flutter-plugins-dependencies instead of grafting tvOS entries onto it. Every federated tvOS plugin with dartPluginClass: was silently dropped from the generated dart_plugin_registrant.dart, breaking the federated plugin chain at runtime.

The bug

ensureReadyForTvosTooling constructed a fresh dependenciesJson and wrote it over the file that stock flutter pub get had just populated:

final dependenciesJson = <String, dynamic>{
  'info': '...',
  'plugins': <String, dynamic>{'tvos': tvosPluginEntries},   // only `tvos` key
  'dependencyGraph': <dynamic>[],                            // reset to empty!
};

That wiped the ios / android / macos / web plugin lists and the dependencyGraph written by stock pub get.

Later in the build pipeline, writeTvosDartPluginRegistrant calls _discoverTvosPlugins again, which walks dependencyGraph to find tvOS plugins. With the graph emptied, it finds nothing → writes an empty dart_plugin_registrant.dart → federated plugins never call their registerWith().

Visible symptoms

Plugin Pre-fix runtime error
shared_preferences_tvos Bad state: The SharedPreferencesAsyncPlatform instance must be set.
video_player_tvos UnimplementedError on VideoPlayerPlatform.instance.create(...)
path_provider_tvos MissingPluginException(...path_provider...) (transitive via other plugins)
audioplayers_tvos MissingPluginException(...xyz.luan/audioplayers.global...)
flutter_secure_storage_tvos plugin never registered → test runner hung

Each of these declares a dartPluginClass: (e.g. SharedPreferencesFoundation, AVFoundationVideoPlayer, PathProviderTvos). Plugins that only declare pluginClass: (no dartPluginClass:) were unaffected because their registration goes through the Swift GeneratedPluginRegistrant.swift path, which has a separate generator that doesn't depend on dependencyGraph.

The fix

Read the existing .flutter-plugins-dependencies (falling back to a fresh skeleton if absent or malformed) and only graft the tvOS plugin entries onto the plugins map. ios / android / macos / web entries and the dependencyGraph written by stock pub get are preserved.

var dependenciesJson = <String, dynamic>{
  'info': '...',
  'plugins': <String, dynamic>{},
  'dependencyGraph': <dynamic>[],
};
final File depsFile = project.flutterPluginsDependenciesFile;
if (depsFile.existsSync()) {
  try {
    dependenciesJson = json.decode(depsFile.readAsStringSync()) as Map<String, dynamic>;
  } on FormatException { /* fall back */ }
  on FileSystemException { /* fall back */ }
}
final pluginsMap = (dependenciesJson['plugins'] as Map<String, dynamic>?) ?? <String, dynamic>{};
pluginsMap['tvos'] = tvosPluginEntries;
dependenciesJson['plugins'] = pluginsMap;

Scope / cross-platform safety

ensureReadyForTvosTooling early-returns on line 280 if no tvos/ directory exists:

if (!tvosDir.existsSync()) {
  return;
}

So projects without a tvOS target (pure iOS / Android / web / macOS / Linux / Windows) hit the early return and see zero behavioural change. The fix is strictly additive for tvOS projects — it preserves data that was previously being thrown away.

Test plan

Tested against fluttertv/plugins integration suite (11 plugins) with Flutter 3.44.0:

Before fix After fix
Unit tests 11/11 pass 11/11 pass
Integration pass 5 7
Integration fail 4 (registrant errors) 2 (real plugin bugs)
Wall time hung / 24 min 14 min

Newly passing integration: flutter_secure_storage_tvos, video_player_tvos.

Massively improved (failures now visible as real plugin bugs rather than masked by registration errors):

  • shared_preferences_tvos: was +63 -23 → now +85 -1
  • audioplayers_tvos: was hung at +2 -40 → now +53 -1

The 2 remaining failures are real plugin logic bugs unrelated to this fix (a migration tool ordering bug in shared_preferences and one audioplayers test).

Related

Pairs with fluttertv/plugins@48c967e which untracks the stale empty GeneratedPluginRegistrant.swift stubs from each plugin's example app. Together these two commits fully restore federated tvOS plugin registration on Flutter 3.44.

`ensureReadyForTvosTooling` was constructing a fresh `dependenciesJson`
from scratch and writing it over the file that stock `flutter pub get`
had just populated. The freshly written file kept only the `tvos` key
and reset `dependencyGraph` to `[]`. That broke every later
`_discoverTvosPlugins` call (notably the one inside
`writeTvosDartPluginRegistrant` during the build pipeline) because
the discovery walks `dependencyGraph` — empty graph → no plugins →
empty `dart_plugin_registrant.dart`.

Visible symptoms before this fix: federated tvOS plugins with
`dartPluginClass:` (e.g. `shared_preferences_tvos`, `video_player_tvos`,
`path_provider_tvos`) silently dropped from the registrant on every
build, surfacing at runtime as `MissingPluginException` for legacy
APIs or `Bad state: <X>Platform.instance must be set` for the newer
async APIs. Integration test impact: `shared_preferences` went
+85/-1 instead of +63/-23, `audioplayers` from a hang to +53/-1,
`flutter_secure_storage` and `video_player` from fail/timeout to
fully green.

The fix reads the existing file (falling back to a fresh skeleton if
absent or malformed) and only grafts the tvOS plugin entries onto the
`plugins` map. iOS/Android/macOS/web entries and the `dependencyGraph`
written by stock pub get are preserved untouched.

Function is tvOS-scoped via the early `if (!tvosDir.existsSync()) return`
at the top, so non-tvOS projects see no behavioural change.
Copilot AI review requested due to automatic review settings May 26, 2026 06:43
@MAUstaoglu MAUstaoglu requested review from DenisovAV and removed request for Copilot May 26, 2026 06:44
Comment thread lib/tvos_plugins.dart
// Malformed file — fall back to a fresh skeleton (defined above).
} on FileSystemException {
// File disappeared between existsSync() and read — same fallback.
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

json.decode(...) as Map<String, dynamic> throws TypeError (not FormatException) when the file contains valid JSON whose root is not an object — e.g. [], null, or a bare number. TypeError is not caught by either branch here, so it propagates out of ensureReadyForTvosTooling and crashes the build with a raw Dart stack trace.

The existing _walkPluginDependencies reader (same file, ~line 105) already guards against this. Same fix here:

try {
  final decoded = json.decode(depsFile.readAsStringSync());
  if (decoded is Map<String, dynamic>) {
    dependenciesJson = decoded;
  } else {
    globals.logger.printWarning(
      '.flutter-plugins-dependencies is not a JSON object; regenerating from scratch.',
    );
  }
} on FormatException catch (e) {
  globals.logger.printWarning(
    '.flutter-plugins-dependencies contains malformed JSON ($e); regenerating from scratch.',
  );
} on FileSystemException catch (e) {
  globals.logger.printWarning(
    '.flutter-plugins-dependencies disappeared before it could be read ($e); regenerating from scratch.',
  );
}

Also worth adding a printWarning to the FormatException/FileSystemException branches — silent swallowing of a corrupt file makes debugging hard.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — fixed in edb2d98 with the type-check pattern you suggested, plus the printWarning for both FormatException and FileSystemException branches. Silent fallback was indeed making the original bug hard to debug.

Comment thread lib/tvos_plugins.dart
dependenciesJson['plugins'] = pluginsMap;

final pluginsBuffer = StringBuffer();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same TypeError risk here: if the file was read successfully but plugins is not a Map (e.g. "plugins": []), the as Map<String, dynamic>? cast throws outside the try/catch and crashes.

Safer:

final rawPlugins = dependenciesJson['plugins'];
final pluginsMap = rawPlugins is Map<String, dynamic>
    ? rawPlugins
    : <String, dynamic>{};

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same pattern applied in edb2d98rawPlugins is Map<String, dynamic> ? rawPlugins : <String, dynamic>{}. Also fixed an identical latent TypeError you mentioned was guarded in _walkPluginDependencies line 105 but actually wasn't — the regression test for non-object root caught it.

@DenisovAV
Copy link
Copy Markdown
Contributor

Overall: fix is correct in intent, two implementation issues flagged inline.

Missing test: the core invariant — that dependencyGraph and other platform keys (ios/android) survive a call to ensureReadyForTvosTooling — has no coverage. Given that the original bug was silent and production-impacting, a regression test would be valuable:

  1. Write a .flutter-plugins-dependencies file with a real dependencyGraph array and existing ios plugin keys
  2. Call ensureReadyForTvosTooling
  3. Re-read the file and assert dependencyGraph is unchanged and plugins.ios is still present alongside the new plugins.tvos

…on tests

Addresses three review items on PR fluttertv#15:

1. `ensureReadyForTvosTooling` — replace the blind
   `as Map<String, dynamic>` cast on the parsed
   `.flutter-plugins-dependencies` with a runtime type-check.
   A valid-JSON-but-non-object root (`[]`, `null`, a bare number)
   would otherwise throw `TypeError` outside the existing
   `FormatException`/`FileSystemException` handlers and crash the
   build. Same change applied to the `dependenciesJson['plugins']`
   cast: a wrong-shaped `plugins: []` value now falls back to an
   empty map instead of throwing.

2. The two `FormatException` / `FileSystemException` branches now
   emit `printWarning` messages explaining the fallback. Silent
   swallowing was making the original
   `dependencyGraph`-wipe bug nearly impossible to debug.

3. `_walkPluginDependencies` had the same latent
   `TypeError` (line 105) — caught by the new regression tests
   while writing them. Fixed with the same type-check pattern;
   the regression tests would have failed without it.

Adds three test cases under the existing
`ensureReadyForTvosTooling end-to-end` group:

- preserves dependencyGraph + ios/android plugin keys (the core
  regression test the reviewer asked for — writes a realistic
  pub-get-shaped file, calls the function, asserts everything
  except the `tvos` graft is bit-identical)
- falls back when root JSON is not an object (e.g. `[]`)
- falls back when `plugins` key is the wrong shape (e.g. an array)

All 29 tests in tvos_plugins_test.dart pass.
Copilot AI review requested due to automatic review settings May 26, 2026 07:51
@MAUstaoglu
Copy link
Copy Markdown
Member Author

Thanks for the careful review — all three items addressed in edb2d98:

  1. Line 326 TypeError — replaced blind as Map<String, dynamic> cast with the type-check pattern you suggested, plus printWarning on both fallback branches.
  2. Line 336 TypeError — same fix on the plugins key access. While writing the regression test I also found and fixed the same latent bug in _walkPluginDependencies (line 105) — the suggested test exercised both code paths and surfaced it.
  3. Regression test — added three under the existing ensureReadyForTvosTooling end-to-end group:
    • preserves dependencyGraph and existing platform plugin keys — the core invariant you asked for: writes a realistic pub-get-shaped file, calls ensureReadyForTvosTooling, asserts everything except the new tvos key graft is bit-identical (ios/android entries + dependencyGraph survive).
    • falls back when .flutter-plugins-dependencies root is not an object — exercises the []/null/bare-number case.
    • falls back when plugins key exists but is not a map — exercises the wrong-shape plugins: [] case.

All 29 tests in tvos_plugins_test.dart pass locally. Ready for re-review.

@MAUstaoglu MAUstaoglu requested review from DenisovAV and removed request for Copilot May 26, 2026 07:53
Copy link
Copy Markdown
Contributor

@DenisovAV DenisovAV left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All issues addressed. TypeError handled with type guard, pluginsMap extraction safe, regression tests added. LGTM.

@DenisovAV DenisovAV merged commit cd4c701 into fluttertv:main May 26, 2026
2 checks passed
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.

2 participants