[tool] Don't write the .flutter-plugins-dependencies file if it is unchanged (#166164)

Currently, `refreshPlugins` always updates the
`.flutter-plugins-dependencies` file. In a future change, `flutter
assemble` will use the `.flutter-plugins-dependencies` file as an input.
Unnecessary writes to this file will invalidate build targets.

This updates the logic to only write .flutter-plugins-dependencies if it
has "significant" changes.

Part of https://github.com/flutter/flutter/issues/163874

Next pull request: https://github.com/flutter/flutter/pull/165916

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md

---------

Co-authored-by: Victoria Ashworth <15619084+vashworth@users.noreply.github.com>
This commit is contained in:
Loïc Sharma
2025-03-31 15:39:05 -07:00
committed by GitHub
parent f539bb4471
commit 9b4164035b
2 changed files with 149 additions and 52 deletions

View File

@@ -234,26 +234,79 @@ bool _writeFlutterPluginsList(
'macos': swiftPackageManagerEnabledMacos,
};
// Only notify if the plugins list has changed. [date_created] will always be different,
// [version] is not relevant for this check.
final String? oldPluginsFileStringContent = _readFileContent(pluginsFile);
bool pluginsChanged = true;
if (oldPluginsFileStringContent != null) {
try {
final Object? decodedJson = jsonDecode(oldPluginsFileStringContent);
if (decodedJson is Map<String, Object?>) {
final String jsonOfNewPluginsMap = jsonEncode(pluginsMap);
final String jsonOfOldPluginsMap = jsonEncode(decodedJson[_kFlutterPluginsPluginListKey]);
pluginsChanged = jsonOfNewPluginsMap != jsonOfOldPluginsMap;
}
} on FormatException catch (_) {}
// Only write the plugins file if its content have changed. This ensures
// that flutter assemble targets that depend on this file aren't invalidated
// unnecessarily.
final (:bool pluginsChanged, :bool contentsChanged) = _detectFlutterPluginsListChanges(
pluginsFile,
result,
);
if (pluginsChanged || contentsChanged) {
final String pluginFileContent = json.encode(result);
pluginsFile.writeAsStringSync(pluginFileContent, flush: true);
}
final String pluginFileContent = json.encode(result);
pluginsFile.writeAsStringSync(pluginFileContent, flush: true);
return pluginsChanged;
}
/// Find what has changed in the .flutter-plugins-dependencies JSON file.
///
/// [pluginsChanged] is [true] if anything changed in the 'plugins' property.
/// This indicates that platform-specific tooling like 'pod install' should be
/// re-run.
///
/// [contentsChanged] is [true] if [newJson] has changes that should be saved to
/// disk.
({bool pluginsChanged, bool contentsChanged}) _detectFlutterPluginsListChanges(
File pluginsFile,
Map<String, Object> newJson,
) {
final String? oldPluginsFileStringContent = _readFileContent(pluginsFile);
if (oldPluginsFileStringContent == null) {
return (pluginsChanged: true, contentsChanged: true);
}
try {
final Object? oldJson = jsonDecode(oldPluginsFileStringContent);
if (oldJson is! Map<String, Object?>) {
return (pluginsChanged: true, contentsChanged: true);
}
// Check if plugins changed.
final String jsonOfNewPluginsMap = jsonEncode(newJson[_kFlutterPluginsPluginListKey]);
final String jsonOfOldPluginsMap = jsonEncode(oldJson[_kFlutterPluginsPluginListKey]);
if (jsonOfNewPluginsMap != jsonOfOldPluginsMap) {
return (pluginsChanged: true, contentsChanged: true);
}
// Create a copy of the new JSON so that the input isn't mutated when we
// drop properties that should be ignored.
final Map<String, Object?> newJsonCopy = <String, Object?>{...newJson};
// The 'info' property is a comment that doesn't affect functionality.
// The 'dependencyGraph' property is deprecated and shouldn't be used.
// The 'date_created' property is always updated.
// The 'plugins' property has already been checked by the logic above.
const List<String> ignoredKeys = <String>[
'info',
'dependencyGraph',
'date_created',
_kFlutterPluginsPluginListKey,
];
for (final String key in ignoredKeys) {
oldJson.remove(key);
newJsonCopy.remove(key);
}
final String old = jsonEncode(oldJson);
final String updated = jsonEncode(newJsonCopy);
return (pluginsChanged: false, contentsChanged: old != updated);
} on FormatException catch (_) {
return (pluginsChanged: true, contentsChanged: true);
}
}
/// Creates a map representation of the [plugins] for those supported by [platformKey].
/// All given [plugins] must provide an implementation for the [platformKey].
List<Map<String, Object>> _createPluginMapOfPlatform(List<Plugin> plugins, String platformKey) {

View File

@@ -409,7 +409,7 @@ dependencies:
() async {
await refreshPluginsList(flutterProject);
expect(flutterProject.flutterPluginsDependenciesFile.existsSync(), false);
expect(flutterProject.flutterPluginsDependenciesFile, isNot(exists));
},
overrides: <Type, Generator>{
FileSystem: () => fs,
@@ -426,7 +426,7 @@ dependencies:
await refreshPluginsList(flutterProject);
expect(flutterProject.flutterPluginsDependenciesFile.existsSync(), false);
expect(flutterProject.flutterPluginsDependenciesFile, isNot(exists));
},
overrides: <Type, Generator>{
FileSystem: () => fs,
@@ -450,12 +450,8 @@ dependencies:
await refreshPluginsList(flutterProject);
expect(
flutterProject.flutterPluginsFile.existsSync(),
false,
reason: 'No longer emitted',
);
expect(flutterProject.flutterPluginsDependenciesFile.existsSync(), true);
expect(flutterProject.flutterPluginsFile, isNot(exists), reason: 'No longer emitted');
expect(flutterProject.flutterPluginsDependenciesFile, exists);
final String pluginsFileContents =
flutterProject.flutterPluginsDependenciesFile.readAsStringSync();
@@ -527,8 +523,8 @@ dependencies:
await refreshPluginsList(flutterProject);
// Verify .flutter-plugins-dependencies is configured correctly.
expect(flutterProject.flutterPluginsFile.existsSync(), true);
expect(flutterProject.flutterPluginsDependenciesFile.existsSync(), true);
expect(flutterProject.flutterPluginsFile, exists);
expect(flutterProject.flutterPluginsDependenciesFile, exists);
expect(
flutterProject.flutterPluginsFile.readAsStringSync(),
'# This is a generated file; do not edit or check into version control.\n'
@@ -619,6 +615,54 @@ dependencies:
},
);
testUsingContext(
'Refreshing the plugin list updates .flutter-plugins-dependencies if the plugins changed',
() async {
// Refresh the plugin list (we have no plugins).
await refreshPluginsList(flutterProject);
expect(flutterProject.flutterPluginsDependenciesFile, isNot(exists));
// Create an initial plugin (we previously had none).
createLegacyPluginWithDependencies(name: 'plugin-a', dependencies: <String>[]);
await refreshPluginsList(flutterProject, iosPlatform: true, macOSPlatform: true);
expect(flutterProject.flutterPluginsDependenciesFile, exists);
final FileStat stat1 = flutterProject.flutterPluginsDependenciesFile.statSync();
// Add a new plugin.
createLegacyPluginWithDependencies(name: 'plugin-b', dependencies: <String>[]);
await refreshPluginsList(flutterProject, iosPlatform: true, macOSPlatform: true);
expect(flutterProject.flutterPluginsDependenciesFile, exists);
final FileStat stat2 = flutterProject.flutterPluginsDependenciesFile.statSync();
expect(
stat2.modified.isAfter(stat1.modified),
isTrue,
reason: 'A new plugin was added, .flutter-plugins-dependencies file should be updated.',
);
// Do not add new plugins.
await refreshPluginsList(flutterProject, iosPlatform: true, macOSPlatform: true);
expect(flutterProject.flutterPluginsDependenciesFile, exists);
final FileStat stat3 = flutterProject.flutterPluginsDependenciesFile.statSync();
expect(
stat3.modified,
stat2.modified,
reason: 'No plugins changed, .flutter-plugins-dependencies should not be changed',
);
},
overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
SystemClock: () => systemClock,
FlutterVersion: () => flutterVersion,
Pub: FakePubWithPrimedDeps.new,
// TODO(matanlurey): Remove as part of https://github.com/flutter/flutter/issues/160257.
// Not necessary, you can observe this bug by calling `generateLegacyPlugins: false`,
// but since this flag is about to be enabled, and enabling it implicitly sets that
// argument to false, this is a more "honest" test.
FeatureFlags: enableExplicitPackageDependencies,
},
);
testUsingContext(
'Refreshing the plugin list for iOS/macOS projects invokes invalidatePodInstallOutput if the plugins changed',
() async {
@@ -718,7 +762,7 @@ dependencies:
await refreshPluginsList(flutterProject);
expect(flutterProject.flutterPluginsDependenciesFile.existsSync(), true);
expect(flutterProject.flutterPluginsDependenciesFile, exists);
final String pluginsString =
flutterProject.flutterPluginsDependenciesFile.readAsStringSync();
final Map<String, dynamic> jsonContent =
@@ -808,7 +852,7 @@ dependencies:
await refreshPluginsList(flutterProject, iosPlatform: true, macOSPlatform: true);
expect(flutterProject.flutterPluginsDependenciesFile.existsSync(), true);
expect(flutterProject.flutterPluginsDependenciesFile, exists);
final String pluginsString =
flutterProject.flutterPluginsDependenciesFile.readAsStringSync();
final Map<String, dynamic> jsonContent =
@@ -860,7 +904,7 @@ dependencies:
await refreshPluginsList(flutterProject, forceCocoaPodsOnly: true);
expect(flutterProject.flutterPluginsDependenciesFile.existsSync(), true);
expect(flutterProject.flutterPluginsDependenciesFile, exists);
final String pluginsString =
flutterProject.flutterPluginsDependenciesFile.readAsStringSync();
final Map<String, dynamic> jsonContent =
@@ -912,7 +956,7 @@ dependencies:
await refreshPluginsList(flutterProject, iosPlatform: true, macOSPlatform: true);
expect(flutterProject.flutterPluginsDependenciesFile.existsSync(), true);
expect(flutterProject.flutterPluginsDependenciesFile, exists);
final String pluginsString =
flutterProject.flutterPluginsDependenciesFile.readAsStringSync();
final Map<String, dynamic> jsonContent =
@@ -944,8 +988,8 @@ dependencies:
macosProject.exists = true;
await refreshPluginsList(flutterProject, iosPlatform: true, macOSPlatform: true);
expect(iosProject.podManifestLock.existsSync(), false);
expect(macosProject.podManifestLock.existsSync(), false);
expect(iosProject.podManifestLock, isNot(exists));
expect(macosProject.podManifestLock, isNot(exists));
},
overrides: <Type, Generator>{
FileSystem: () => fs,
@@ -973,8 +1017,8 @@ dependencies:
simulatePodInstallRun(macosProject);
await refreshPluginsList(flutterProject);
expect(iosProject.podManifestLock.existsSync(), true);
expect(macosProject.podManifestLock.existsSync(), true);
expect(iosProject.podManifestLock, exists);
expect(macosProject.podManifestLock, exists);
},
overrides: <Type, Generator>{
FileSystem: () => fs,
@@ -1007,7 +1051,7 @@ dependencies:
)
.childFile('GeneratedPluginRegistrant.java');
expect(registrant.existsSync(), isTrue);
expect(registrant, exists);
expect(registrant.readAsStringSync(), contains('package io.flutter.plugins'));
expect(registrant.readAsStringSync(), contains('class GeneratedPluginRegistrant'));
expect(
@@ -1070,7 +1114,7 @@ dependencies:
)
.childFile('GeneratedPluginRegistrant.java');
expect(registrant.existsSync(), isTrue);
expect(registrant, exists);
expect(registrant.readAsStringSync(), contains('package io.flutter.plugins'));
expect(registrant.readAsStringSync(), contains('class GeneratedPluginRegistrant'));
expect(
@@ -1101,7 +1145,7 @@ dependencies:
)
.childFile('GeneratedPluginRegistrant.java');
expect(registrant.existsSync(), isTrue);
expect(registrant, exists);
expect(registrant.readAsStringSync(), contains('package io.flutter.plugins'));
expect(registrant.readAsStringSync(), contains('class GeneratedPluginRegistrant'));
expect(
@@ -1259,7 +1303,7 @@ dependencies:
.childDirectory('lib')
.childFile('web_plugin_registrant.dart');
expect(registrant.existsSync(), isTrue);
expect(registrant, exists);
expect(
registrant.readAsStringSync(),
contains("import 'package:web_plugin_with_nested/src/web_plugin.dart';"),
@@ -1325,7 +1369,7 @@ dependencies:
.childDirectory('lib')
.childFile('web_plugin_registrant.dart');
expect(registrant.existsSync(), isTrue);
expect(registrant, exists);
expect(
registrant.readAsStringSync(),
contains(
@@ -1533,8 +1577,8 @@ flutter:
'generated_plugin_registrant.cc',
);
expect(registrantHeader.existsSync(), isTrue);
expect(registrantImpl.existsSync(), isTrue);
expect(registrantHeader, exists);
expect(registrantImpl, exists);
expect(
registrantImpl.readAsStringSync(),
contains('some_plugin_register_with_registrar'),
@@ -1593,7 +1637,7 @@ dependencies:
'generated_plugin_registrant.cc',
);
expect(registrantImpl.existsSync(), isTrue);
expect(registrantImpl, exists);
expect(
registrantImpl.readAsStringSync(),
contains('user_selected_url_launcher_linux_register_with_registrar'),
@@ -1666,7 +1710,7 @@ dependencies:
'generated_plugin_registrant.cc',
);
expect(registrantImpl.existsSync(), isTrue);
expect(registrantImpl, exists);
expect(
registrantImpl.readAsStringSync(),
contains('user_selected_url_launcher_linux_register_with_registrar'),
@@ -1756,7 +1800,7 @@ flutter:
final File pluginMakefile = linuxProject.generatedPluginCmakeFile;
expect(pluginMakefile.existsSync(), isTrue);
expect(pluginMakefile, exists);
final String contents = pluginMakefile.readAsStringSync();
expect(contents, contains('some_plugin'));
expect(
@@ -1825,8 +1869,8 @@ flutter:
'generated_plugin_registrant.cc',
);
expect(registrantHeader.existsSync(), isTrue);
expect(registrantImpl.existsSync(), isTrue);
expect(registrantHeader, exists);
expect(registrantImpl, exists);
expect(registrantImpl.readAsStringSync(), contains('SomePluginRegisterWithRegistrar'));
},
overrides: <Type, Generator>{
@@ -1950,7 +1994,7 @@ flutter:
]) {
final File pluginCmakefile = project!.generatedPluginCmakeFile;
expect(pluginCmakefile.existsSync(), isTrue);
expect(pluginCmakefile, exists);
final String contents = pluginCmakefile.readAsStringSync();
expect(contents, contains('add_subdirectory(flutter/ephemeral/.plugin_symlinks'));
}
@@ -2024,7 +2068,7 @@ flutter:
// refreshPluginsList should call createPluginSymlinks.
await refreshPluginsList(flutterProject);
expect(linuxProject.pluginSymlinkDirectory.childLink('some_plugin').existsSync(), true);
expect(linuxProject.pluginSymlinkDirectory.childLink('some_plugin'), exists);
},
overrides: <Type, Generator>{
FileSystem: () => fs,
@@ -2041,7 +2085,7 @@ flutter:
// refreshPluginsList should call createPluginSymlinks.
await refreshPluginsList(flutterProject);
expect(windowsProject.pluginSymlinkDirectory.childLink('some_plugin').existsSync(), true);
expect(windowsProject.pluginSymlinkDirectory.childLink('some_plugin'), exists);
},
overrides: <Type, Generator>{
FileSystem: () => fs,
@@ -2067,7 +2111,7 @@ flutter:
createPluginSymlinks(flutterProject, force: true);
for (final File file in dummyFiles) {
expect(file.existsSync(), false);
expect(file, isNot(exists));
}
},
overrides: <Type, Generator>{
@@ -2096,7 +2140,7 @@ flutter:
await refreshPluginsList(flutterProject);
for (final File file in dummyFiles) {
expect(file.existsSync(), false);
expect(file, isNot(exists));
}
},
overrides: <Type, Generator>{
@@ -2124,7 +2168,7 @@ flutter:
createPluginSymlinks(flutterProject);
for (final File file in dummyFiles) {
expect(file.existsSync(), true);
expect(file, exists);
}
},
overrides: <Type, Generator>{
@@ -2152,7 +2196,7 @@ flutter:
createPluginSymlinks(flutterProject);
for (final Link link in links) {
expect(link.existsSync(), true);
expect(link, exists);
}
},
overrides: <Type, Generator>{