[path_provider] Restore 2.8 compatibility on Android and iOS (#6039)

Adds a new repo tooling command that removes dev_dependencies, which
aren't needed to consume a package, only for development. Also adds
a --lib-only flag to analyze to analyze only the client-facing code.
This is intended for use in the legacy analyze CI steps, primarily to
solve the problem that currently plugins that use Pigeon can't support a
version of Flutter older than the version supported by Pigeon, because
otherwise the legacy analysis CI steps fail.

Adds this new command to the legacy analysis CI step, and restores
the recently-removed 2.8/2.10 compatibility to path_provider.
diff --git a/.cirrus.yml b/.cirrus.yml
index 479d493..c53e665 100644
--- a/.cirrus.yml
+++ b/.cirrus.yml
@@ -163,8 +163,14 @@
         matrix:
           CHANNEL: "2.10.5"
           CHANNEL: "2.8.1"
+      package_prep_script:
+        # Allow analyzing plugins that use a Pigeon version with a higher
+        # minimum Flutter/Dart version than the plugin itself.
+        - ./script/tool_runner.sh remove-dev-dependencies
       analyze_script:
-        - ./script/tool_runner.sh analyze --skip-if-not-supporting-flutter-version="$CHANNEL" --custom-analysis=script/configs/custom_analysis.yaml
+        # Only analyze lib/; non-client code doesn't need to work on
+        # all supported legacy version.
+        - ./script/tool_runner.sh analyze --lib-only --skip-if-not-supporting-flutter-version="$CHANNEL" --custom-analysis=script/configs/custom_analysis.yaml
     - name: readme_excerpts
       env:
         CIRRUS_CLONE_SUBMODULES: true
diff --git a/packages/path_provider/path_provider_android/CHANGELOG.md b/packages/path_provider/path_provider_android/CHANGELOG.md
index c40c103..799c95a 100644
--- a/packages/path_provider/path_provider_android/CHANGELOG.md
+++ b/packages/path_provider/path_provider_android/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 2.0.17
+
+* Lower minimim version back to 2.8.1.
+
 ## 2.0.16
 
 * Fixes bug with `getExternalStoragePaths(null)`.
diff --git a/packages/path_provider/path_provider_android/pubspec.yaml b/packages/path_provider/path_provider_android/pubspec.yaml
index 4c228bb..ae132b0 100644
--- a/packages/path_provider/path_provider_android/pubspec.yaml
+++ b/packages/path_provider/path_provider_android/pubspec.yaml
@@ -2,11 +2,11 @@
 description: Android implementation of the path_provider plugin.
 repository: https://github.com/flutter/plugins/tree/main/packages/path_provider/path_provider_android
 issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+path_provider%22
-version: 2.0.16
+version: 2.0.17
 
 environment:
   sdk: ">=2.14.0 <3.0.0"
-  flutter: ">=3.0.0"
+  flutter: ">=2.8.1"
 
 flutter:
   plugin:
diff --git a/packages/path_provider/path_provider_ios/CHANGELOG.md b/packages/path_provider/path_provider_ios/CHANGELOG.md
index 8569c1b..c9deb3f 100644
--- a/packages/path_provider/path_provider_ios/CHANGELOG.md
+++ b/packages/path_provider/path_provider_ios/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 2.0.11
+
+* Lower minimim version back to 2.8.
+
 ## 2.0.10
 
 * Switches backend to pigeon.
diff --git a/packages/path_provider/path_provider_ios/pubspec.yaml b/packages/path_provider/path_provider_ios/pubspec.yaml
index 16d2f2e..dfa4a87 100644
--- a/packages/path_provider/path_provider_ios/pubspec.yaml
+++ b/packages/path_provider/path_provider_ios/pubspec.yaml
@@ -2,11 +2,11 @@
 description: iOS implementation of the path_provider plugin.
 repository: https://github.com/flutter/plugins/tree/main/packages/path_provider/path_provider_ios
 issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+path_provider%22
-version: 2.0.10
+version: 2.0.11
 
 environment:
   sdk: ">=2.14.0 <3.0.0"
-  flutter: ">=3.0.0"
+  flutter: ">=2.8.0"
 
 flutter:
   plugin:
diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md
index f0534c2..ad3f35a 100644
--- a/script/tool/CHANGELOG.md
+++ b/script/tool/CHANGELOG.md
@@ -1,3 +1,10 @@
+## 0.8.10
+
+- Adds a new `remove-dev-dependencies` command to remove `dev_dependencies`
+  entries to make legacy version analysis possible in more cases.
+- Adds a `--lib-only` option to `analyze` to allow only analyzing the client
+  parts of a library for legacy verison compatibility.
+
 ## 0.8.9
 
 - Includes `dev_dependencies` when overridding dependencies using
diff --git a/script/tool/lib/src/analyze_command.dart b/script/tool/lib/src/analyze_command.dart
index 8778b3d..54b4f33 100644
--- a/script/tool/lib/src/analyze_command.dart
+++ b/script/tool/lib/src/analyze_command.dart
@@ -32,10 +32,13 @@
         valueHelp: 'dart-sdk',
         help: 'An optional path to a Dart SDK; this is used to override the '
             'SDK used to provide analysis.');
+    argParser.addFlag(_libOnlyFlag,
+        help: 'Only analyze the lib/ directory of the main package, not the '
+            'entire package.');
   }
 
   static const String _customAnalysisFlag = 'custom-analysis';
-
+  static const String _libOnlyFlag = 'lib-only';
   static const String _analysisSdk = 'analysis-sdk';
 
   late String _dartBinaryPath;
@@ -104,13 +107,20 @@
 
   @override
   Future<PackageResult> runForPackage(RepositoryPackage package) async {
-    // Analysis runs over the package and all subpackages, so all of them need
-    // `flutter pub get` run before analyzing. `example` packages can be
-    // skipped since 'flutter packages get' automatically runs `pub get` in
-    // examples as part of handling the parent directory.
+    final bool libOnly = getBoolArg(_libOnlyFlag);
+
+    if (libOnly && !package.libDirectory.existsSync()) {
+      return PackageResult.skip('No lib/ directory.');
+    }
+
+    // Analysis runs over the package and all subpackages (unless only lib/ is
+    // being analyzed), so all of them need `flutter pub get` run before
+    // analyzing. `example` packages can be skipped since 'flutter packages get'
+    // automatically runs `pub get` in examples as part of handling the parent
+    // directory.
     final List<RepositoryPackage> packagesToGet = <RepositoryPackage>[
       package,
-      ...await getSubpackages(package).toList(),
+      if (!libOnly) ...await getSubpackages(package).toList(),
     ];
     for (final RepositoryPackage packageToGet in packagesToGet) {
       if (packageToGet.directory.basename != 'example' ||
@@ -129,8 +139,8 @@
     if (_hasUnexpecetdAnalysisOptions(package)) {
       return PackageResult.fail(<String>['Unexpected local analysis options']);
     }
-    final int exitCode = await processRunner.runAndStream(
-        _dartBinaryPath, <String>['analyze', '--fatal-infos'],
+    final int exitCode = await processRunner.runAndStream(_dartBinaryPath,
+        <String>['analyze', '--fatal-infos', if (libOnly) 'lib'],
         workingDir: package.directory);
     if (exitCode != 0) {
       return PackageResult.fail();
diff --git a/script/tool/lib/src/main.dart b/script/tool/lib/src/main.dart
index 966e7b6..ea1ec06 100644
--- a/script/tool/lib/src/main.dart
+++ b/script/tool/lib/src/main.dart
@@ -28,6 +28,7 @@
 import 'publish_plugin_command.dart';
 import 'pubspec_check_command.dart';
 import 'readme_check_command.dart';
+import 'remove_dev_dependencies.dart';
 import 'test_command.dart';
 import 'update_excerpts_command.dart';
 import 'update_release_info_command.dart';
@@ -71,6 +72,7 @@
     ..addCommand(PublishPluginCommand(packagesDir))
     ..addCommand(PubspecCheckCommand(packagesDir))
     ..addCommand(ReadmeCheckCommand(packagesDir))
+    ..addCommand(RemoveDevDependenciesCommand(packagesDir))
     ..addCommand(TestCommand(packagesDir))
     ..addCommand(UpdateExcerptsCommand(packagesDir))
     ..addCommand(UpdateReleaseInfoCommand(packagesDir))
diff --git a/script/tool/lib/src/remove_dev_dependencies.dart b/script/tool/lib/src/remove_dev_dependencies.dart
new file mode 100644
index 0000000..3085e0d
--- /dev/null
+++ b/script/tool/lib/src/remove_dev_dependencies.dart
@@ -0,0 +1,58 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:file/file.dart';
+import 'package:yaml/yaml.dart';
+import 'package:yaml_edit/yaml_edit.dart';
+
+import 'common/package_looping_command.dart';
+import 'common/repository_package.dart';
+
+/// A command to remove dev_dependencies, which are not used by package clients.
+///
+/// This is intended for use with legacy Flutter version testing, to allow
+/// running analysis (with --lib-only) with versions that are supported for
+/// clients of the library, but not for development of the library.
+class RemoveDevDependenciesCommand extends PackageLoopingCommand {
+  /// Creates a publish metadata updater command instance.
+  RemoveDevDependenciesCommand(Directory packagesDir) : super(packagesDir);
+
+  @override
+  final String name = 'remove-dev-dependencies';
+
+  @override
+  final String description = 'Removes any dev_dependencies section from a '
+      'package, to allow more legacy testing.';
+
+  @override
+  bool get hasLongOutput => false;
+
+  @override
+  PackageLoopingType get packageLoopingType =>
+      PackageLoopingType.includeAllSubpackages;
+
+  @override
+  Future<PackageResult> runForPackage(RepositoryPackage package) async {
+    bool changed = false;
+    final YamlEditor editablePubspec =
+        YamlEditor(package.pubspecFile.readAsStringSync());
+    const String devDependenciesKey = 'dev_dependencies';
+    final YamlNode root = editablePubspec.parseAt(<String>[]);
+    final YamlMap? devDependencies =
+        (root as YamlMap)[devDependenciesKey] as YamlMap?;
+    if (devDependencies != null) {
+      changed = true;
+      print('${indentation}Removed dev_dependencies');
+      editablePubspec.remove(<String>[devDependenciesKey]);
+    }
+
+    if (changed) {
+      package.pubspecFile.writeAsStringSync(editablePubspec.toString());
+    }
+
+    return changed
+        ? PackageResult.success()
+        : PackageResult.skip('Nothing to remove.');
+  }
+}
diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml
index a4ecbfb..2eee1e3 100644
--- a/script/tool/pubspec.yaml
+++ b/script/tool/pubspec.yaml
@@ -1,7 +1,7 @@
 name: flutter_plugin_tools
 description: Productivity utils for flutter/plugins and flutter/packages
 repository: https://github.com/flutter/plugins/tree/main/script/tool
-version: 0.8.9
+version: 0.8.10
 
 dependencies:
   args: ^2.1.0
diff --git a/script/tool/test/analyze_command_test.dart b/script/tool/test/analyze_command_test.dart
index a4a47a2..d3abf0b 100644
--- a/script/tool/test/analyze_command_test.dart
+++ b/script/tool/test/analyze_command_test.dart
@@ -93,6 +93,59 @@
         ]));
   });
 
+  test('passes lib/ directory with --lib-only', () async {
+    final RepositoryPackage package =
+        createFakePackage('a_package', packagesDir);
+
+    await runCapturingPrint(runner, <String>['analyze', '--lib-only']);
+
+    expect(
+        processRunner.recordedCalls,
+        orderedEquals(<ProcessCall>[
+          ProcessCall('flutter', const <String>['pub', 'get'], package.path),
+          ProcessCall('dart', const <String>['analyze', '--fatal-infos', 'lib'],
+              package.path),
+        ]));
+  });
+
+  test('skips when missing lib/ directory with --lib-only', () async {
+    final RepositoryPackage package =
+        createFakePackage('a_package', packagesDir);
+    package.libDirectory.deleteSync();
+
+    final List<String> output =
+        await runCapturingPrint(runner, <String>['analyze', '--lib-only']);
+
+    expect(processRunner.recordedCalls, isEmpty);
+    expect(
+      output,
+      containsAllInOrder(<Matcher>[
+        contains('SKIPPING: No lib/ directory'),
+      ]),
+    );
+  });
+
+  test(
+      'does not run flutter pub get for non-example subpackages with --lib-only',
+      () async {
+    final RepositoryPackage mainPackage = createFakePackage('a', packagesDir);
+    final Directory otherPackagesDir =
+        mainPackage.directory.childDirectory('other_packages');
+    createFakePackage('subpackage1', otherPackagesDir);
+    createFakePackage('subpackage2', otherPackagesDir);
+
+    await runCapturingPrint(runner, <String>['analyze', '--lib-only']);
+
+    expect(
+        processRunner.recordedCalls,
+        orderedEquals(<ProcessCall>[
+          ProcessCall(
+              'flutter', const <String>['pub', 'get'], mainPackage.path),
+          ProcessCall('dart', const <String>['analyze', '--fatal-infos', 'lib'],
+              mainPackage.path),
+        ]));
+  });
+
   test("don't elide a non-contained example package", () async {
     final RepositoryPackage plugin1 = createFakePlugin('a', packagesDir);
     final RepositoryPackage plugin2 = createFakePlugin('example', packagesDir);
diff --git a/script/tool/test/remove_dev_dependencies_test.dart b/script/tool/test/remove_dev_dependencies_test.dart
new file mode 100644
index 0000000..6b21287
--- /dev/null
+++ b/script/tool/test/remove_dev_dependencies_test.dart
@@ -0,0 +1,102 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:args/command_runner.dart';
+import 'package:file/file.dart';
+import 'package:file/memory.dart';
+import 'package:flutter_plugin_tools/src/remove_dev_dependencies.dart';
+import 'package:test/test.dart';
+
+import 'util.dart';
+
+void main() {
+  late FileSystem fileSystem;
+  late Directory packagesDir;
+  late CommandRunner<void> runner;
+
+  setUp(() {
+    fileSystem = MemoryFileSystem();
+    packagesDir = createPackagesDirectory(fileSystem: fileSystem);
+
+    final RemoveDevDependenciesCommand command = RemoveDevDependenciesCommand(
+      packagesDir,
+    );
+    runner = CommandRunner<void>('trim_dev_dependencies_command',
+        'Test for trim_dev_dependencies_command');
+    runner.addCommand(command);
+  });
+
+  void _addToPubspec(RepositoryPackage package, String addition) {
+    final String originalContent = package.pubspecFile.readAsStringSync();
+    package.pubspecFile.writeAsStringSync('''
+$originalContent
+$addition
+''');
+  }
+
+  test('skips if nothing is removed', () async {
+    createFakePackage('a_package', packagesDir, version: '1.0.0');
+
+    final List<String> output =
+        await runCapturingPrint(runner, <String>['remove-dev-dependencies']);
+
+    expect(
+      output,
+      containsAllInOrder(<Matcher>[
+        contains('SKIPPING: Nothing to remove.'),
+      ]),
+    );
+  });
+
+  test('removes dev_dependencies', () async {
+    final RepositoryPackage package =
+        createFakePackage('a_package', packagesDir, version: '1.0.0');
+
+    _addToPubspec(package, '''
+dev_dependencies:
+  some_dependency: ^2.1.8
+  another_dependency: ^1.0.0
+''');
+
+    final List<String> output =
+        await runCapturingPrint(runner, <String>['remove-dev-dependencies']);
+
+    expect(
+      output,
+      containsAllInOrder(<Matcher>[
+        contains('Removed dev_dependencies'),
+      ]),
+    );
+    expect(package.pubspecFile.readAsStringSync(),
+        isNot(contains('some_dependency:')));
+    expect(package.pubspecFile.readAsStringSync(),
+        isNot(contains('another_dependency:')));
+  });
+
+  test('removes from examples', () async {
+    final RepositoryPackage package =
+        createFakePackage('a_package', packagesDir, version: '1.0.0');
+
+    final RepositoryPackage example = package.getExamples().first;
+    _addToPubspec(example, '''
+dev_dependencies:
+  some_dependency: ^2.1.8
+  another_dependency: ^1.0.0
+''');
+
+    final List<String> output =
+        await runCapturingPrint(runner, <String>['remove-dev-dependencies']);
+
+    expect(
+      output,
+      containsAllInOrder(<Matcher>[
+        contains('Removed dev_dependencies'),
+      ]),
+    );
+    expect(package.pubspecFile.readAsStringSync(),
+        isNot(contains('some_dependency:')));
+    expect(package.pubspecFile.readAsStringSync(),
+        isNot(contains('another_dependency:')));
+  });
+}