Generate a makefile for Linux plugins (#51520)

When generating the plugin registrant for Linux, also generate a
makefile that can be included in the app-level Makefile to manage all of
the plugin targets and flags, exporting them in a few known variables
for use in the outer makefile.

Part of #32720
diff --git a/packages/flutter_tools/lib/src/plugins.dart b/packages/flutter_tools/lib/src/plugins.dart
index e830fee..a0dd1f5 100644
--- a/packages/flutter_tools/lib/src/plugins.dart
+++ b/packages/flutter_tools/lib/src/plugins.dart
@@ -801,6 +801,40 @@
 }
 ''';
 
+const String _linuxPluginMakefileTemplate = '''
+# Plugins to include in the build.
+GENERATED_PLUGINS=\\
+{{#plugins}}
+\t{{name}} \\
+{{/plugins}}
+
+GENERATED_PLUGINS_DIR={{pluginsDir}}
+# A plugin library name plugin name with _plugin appended.
+GENERATED_PLUGIN_LIB_NAMES=\$(foreach plugin,\$(GENERATED_PLUGINS),\$(plugin)_plugin)
+
+# Variables for use in the enclosing Makefile. Changes to these names are
+# breaking changes.
+PLUGIN_TARGETS=\$(GENERATED_PLUGINS)
+PLUGIN_LIBRARIES=\$(foreach plugin,\$(GENERATED_PLUGIN_LIB_NAMES),\\
+\t\$(OUT_DIR)/lib\$(plugin).so)
+PLUGIN_LDFLAGS=\$(patsubst %,-l%,\$(GENERATED_PLUGIN_LIB_NAMES))
+PLUGIN_CPPFLAGS=\$(foreach plugin,\$(GENERATED_PLUGINS),\\
+\t-I\$(GENERATED_PLUGINS_DIR)/\$(plugin)/linux)
+
+# Targets
+
+# Implicit rules don't match phony targets, so list plugin builds explicitly.
+{{#plugins}}
+\$(OUT_DIR)/lib{{name}}_plugin.so: | {{name}}
+{{/plugins}}
+
+.PHONY: \$(GENERATED_PLUGINS)
+\$(GENERATED_PLUGINS):
+	make -C \$(GENERATED_PLUGINS_DIR)/\$@/linux \\
+		OUT_DIR=\$(OUT_DIR) \\
+		FLUTTER_EPHEMERAL_DIR="\$(abspath {{ephemeralDir}})"
+''';
+
 Future<void> _writeIOSPluginRegistrant(FlutterProject project, List<Plugin> plugins) async {
   final List<Map<String, dynamic>> iosPlugins = _extractPlatformMaps(plugins, IOSPlugin.kConfigKey);
   final Map<String, dynamic> context = <String, dynamic>{
@@ -841,12 +875,34 @@
   }
 }
 
-Future<void> _writeLinuxPluginRegistrant(FlutterProject project, List<Plugin> plugins) async {
+Future<void> _writeLinuxPluginFiles(FlutterProject project, List<Plugin> plugins) async {
   final List<Map<String, dynamic>> linuxPlugins = _extractPlatformMaps(plugins, LinuxPlugin.kConfigKey);
+  // The generated makefile is checked in, so can't use absolute paths. It is
+  // included by the main makefile, so relative paths must be relative to that
+  // file's directory.
+  final String makefileDirPath = project.linux.makeFile.parent.absolute.path;
   final Map<String, dynamic> context = <String, dynamic>{
     'plugins': linuxPlugins,
+    'ephemeralDir': globals.fs.path.relative(
+      project.linux.ephemeralDirectory.absolute.path,
+      from: makefileDirPath,
+    ),
+    'pluginsDir': globals.fs.path.relative(
+      project.linux.pluginSymlinkDirectory.absolute.path,
+      from: makefileDirPath,
+    ),
   };
   await _writeCppPluginRegistrant(project.linux.managedDirectory, context);
+  await _writeLinuxPluginMakefile(project.linux.managedDirectory, context);
+}
+
+Future<void> _writeLinuxPluginMakefile(Directory destination, Map<String, dynamic> templateContext) async {
+  final String registryDirectory = destination.path;
+  _renderTemplateToFile(
+    _linuxPluginMakefileTemplate,
+    templateContext,
+    globals.fs.path.join(registryDirectory, 'generated_plugins.mk'),
+  );
 }
 
 Future<void> _writeMacOSPluginRegistrant(FlutterProject project, List<Plugin> plugins) async {
@@ -1034,7 +1090,7 @@
   // desktop in existing projects are in place. For now, ignore checkProjects
   // on desktop and always treat it as true.
   if (featureFlags.isLinuxEnabled && project.linux.existsSync()) {
-    await _writeLinuxPluginRegistrant(project, plugins);
+    await _writeLinuxPluginFiles(project, plugins);
   }
   if (featureFlags.isMacOSEnabled && project.macos.existsSync()) {
     await _writeMacOSPluginRegistrant(project, plugins);
diff --git a/packages/flutter_tools/lib/src/project.dart b/packages/flutter_tools/lib/src/project.dart
index 06ca2d6..4a9944e 100644
--- a/packages/flutter_tools/lib/src/project.dart
+++ b/packages/flutter_tools/lib/src/project.dart
@@ -1003,6 +1003,9 @@
   /// the build.
   File get generatedMakeConfigFile => ephemeralDirectory.childFile('generated_config.mk');
 
+  /// Makefile with rules and variables for plugin builds.
+  File get generatedPluginMakeFile => managedDirectory.childFile('generated_plugins.mk');
+
   /// The directory to write plugin symlinks.
   Directory get pluginSymlinkDirectory => ephemeralDirectory.childDirectory('.plugin_symlinks');
 
diff --git a/packages/flutter_tools/test/general.shard/plugins_test.dart b/packages/flutter_tools/test/general.shard/plugins_test.dart
index 7fb5e87..50bb3db 100644
--- a/packages/flutter_tools/test/general.shard/plugins_test.dart
+++ b/packages/flutter_tools/test/general.shard/plugins_test.dart
@@ -83,7 +83,13 @@
       linuxProject = MockLinuxProject();
       when(flutterProject.linux).thenReturn(linuxProject);
       when(linuxProject.pluginConfigKey).thenReturn('linux');
-      when(linuxProject.pluginSymlinkDirectory).thenReturn(flutterProject.directory.childDirectory('linux').childDirectory('symlinks'));
+      final Directory linuxManagedDirectory = flutterProject.directory.childDirectory('linux').childDirectory('flutter');
+      final Directory linuxEphemeralDirectory = linuxManagedDirectory.childDirectory('ephemeral');
+      when(linuxProject.managedDirectory).thenReturn(linuxManagedDirectory);
+      when(linuxProject.ephemeralDirectory).thenReturn(linuxEphemeralDirectory);
+      when(linuxProject.pluginSymlinkDirectory).thenReturn(linuxEphemeralDirectory.childDirectory('.plugin_symlinks'));
+      when(linuxProject.makeFile).thenReturn(linuxManagedDirectory.parent.childFile('Makefile'));
+      when(linuxProject.generatedPluginMakeFile).thenReturn(linuxManagedDirectory.childFile('generated_plugins.mk'));
       when(linuxProject.existsSync()).thenReturn(false);
 
       when(mockClock.now()).thenAnswer(
@@ -871,6 +877,51 @@
         FeatureFlags: () => featureFlags,
       });
 
+      testUsingContext('Injecting creates generated Linux registrant', () async {
+        when(linuxProject.existsSync()).thenReturn(true);
+        when(featureFlags.isLinuxEnabled).thenReturn(true);
+        when(flutterProject.isModule).thenReturn(false);
+        configureDummyPackageAsPlugin();
+
+        await injectPlugins(flutterProject, checkProjects: true);
+
+        final File registrantHeader = linuxProject.managedDirectory.childFile('generated_plugin_registrant.h');
+        final File registrantImpl = linuxProject.managedDirectory.childFile('generated_plugin_registrant.cc');
+
+        expect(registrantHeader.existsSync(), isTrue);
+        expect(registrantImpl.existsSync(), isTrue);
+        expect(registrantImpl.readAsStringSync(), contains('SomePluginRegisterWithRegistrar'));
+      }, overrides: <Type, Generator>{
+        FileSystem: () => fs,
+        ProcessManager: () => FakeProcessManager.any(),
+        FeatureFlags: () => featureFlags,
+      });
+
+      testUsingContext('Injecting creates generated Linux plugin makefile', () async {
+        when(linuxProject.existsSync()).thenReturn(true);
+        when(featureFlags.isLinuxEnabled).thenReturn(true);
+        when(flutterProject.isModule).thenReturn(false);
+        configureDummyPackageAsPlugin();
+
+        await injectPlugins(flutterProject, checkProjects: true);
+
+        final File pluginMakefile = linuxProject.generatedPluginMakeFile;
+
+        expect(pluginMakefile.existsSync(), isTrue);
+        final String contents = pluginMakefile.readAsStringSync();
+        expect(contents, contains('libapackage_plugin.so'));
+        // Verify all the variables the app-level Makefile rely on.
+        expect(contents, contains('PLUGIN_TARGETS='));
+        expect(contents, contains('PLUGIN_LIBRARIES='));
+        expect(contents, contains('PLUGIN_LDFLAGS='));
+        expect(contents, contains('PLUGIN_CPPFLAGS='));
+      }, overrides: <Type, Generator>{
+        FileSystem: () => fs,
+        ProcessManager: () => FakeProcessManager.any(),
+        FeatureFlags: () => featureFlags,
+      });
+
+
       testUsingContext('Injecting creates generated Windows registrant', () async {
         when(windowsProject.existsSync()).thenReturn(true);
         when(featureFlags.isWindowsEnabled).thenReturn(true);