[flutter_plugin_tools] Migrate more commands to NNBD (#4026)

Migrates:
- `all_plugins_app`
- `podspecs`
- `firebase-test-lab`

Minor functional changes to `firebase-test-lab` based on issues highlighted by the migration:
- The build ID used in the path is now a) passable, and b) given a fallback value in the path that
  isn't "null"
- Flag setup will no longer assume that `$HOME` must be set in the environment.
- Adds a --build-id flag to `firebase-test-lab` instead of hard-coding the use of  `CIRRUS_BUILD_ID`.
  The default is still `CIRRUS_BUILD_ID` so no CI changes are needed.

Part of https://github.com/flutter/flutter/issues/81912
diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md
index bd0875c..2ada2cc 100644
--- a/script/tool/CHANGELOG.md
+++ b/script/tool/CHANGELOG.md
@@ -1,3 +1,9 @@
+## NEXT
+
+- Add a --build-id flag to `firebase-test-lab` instead of hard-coding the use of
+  `CIRRUS_BUILD_ID`. `CIRRUS_BUILD_ID` is the default value for that flag, for backward
+  compatibility.
+
 ## 0.2.0
 
 - Remove `xctest`'s `--skip`, which is redundant with `--ignore`.
diff --git a/script/tool/lib/src/create_all_plugins_app_command.dart b/script/tool/lib/src/create_all_plugins_app_command.dart
index 9de7f1b..cd5b85e 100644
--- a/script/tool/lib/src/create_all_plugins_app_command.dart
+++ b/script/tool/lib/src/create_all_plugins_app_command.dart
@@ -2,9 +2,6 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-// @dart=2.9
-
-import 'dart:async';
 import 'dart:io' as io;
 
 import 'package:file/file.dart';
@@ -18,17 +15,17 @@
   /// Creates an instance of the builder command.
   CreateAllPluginsAppCommand(
     Directory packagesDir, {
-    this.pluginsRoot,
-  }) : super(packagesDir) {
-    pluginsRoot ??= packagesDir.fileSystem.currentDirectory;
-    appDirectory = pluginsRoot.childDirectory('all_plugins');
+    Directory? pluginsRoot,
+  })  : pluginsRoot = pluginsRoot ?? packagesDir.fileSystem.currentDirectory,
+        super(packagesDir) {
+    appDirectory = this.pluginsRoot.childDirectory('all_plugins');
   }
 
   /// The root directory of the plugin repository.
   Directory pluginsRoot;
 
   /// The location of the synthesized app project.
-  Directory appDirectory;
+  late Directory appDirectory;
 
   @override
   String get description =>
@@ -177,7 +174,7 @@
 
 version: ${pubspec.version}
 
-environment:${_pubspecMapString(pubspec.environment)}
+environment:${_pubspecMapString(pubspec.environment!)}
 
 dependencies:${_pubspecMapString(pubspec.dependencies)}
 
diff --git a/script/tool/lib/src/firebase_test_lab_command.dart b/script/tool/lib/src/firebase_test_lab_command.dart
index 6db0d62..741d856 100644
--- a/script/tool/lib/src/firebase_test_lab_command.dart
+++ b/script/tool/lib/src/firebase_test_lab_command.dart
@@ -2,8 +2,6 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-// @dart=2.9
-
 import 'dart:async';
 import 'dart:io' as io;
 
@@ -27,15 +25,25 @@
       defaultsTo: 'flutter-infra',
       help: 'The Firebase project name.',
     );
+    final String? homeDir = io.Platform.environment['HOME'];
     argParser.addOption('service-key',
         defaultsTo:
-            p.join(io.Platform.environment['HOME'], 'gcloud-service-key.json'));
+            homeDir == null ? null : p.join(homeDir, 'gcloud-service-key.json'),
+        help: 'The path to the service key for gcloud authentication.\n'
+            r'If not provided, \$HOME/gcloud-service-key.json will be '
+            r'assumed if $HOME is set.');
     argParser.addOption('test-run-id',
         defaultsTo: const Uuid().v4(),
         help:
             'Optional string to append to the results path, to avoid conflicts. '
             'Randomly chosen on each invocation if none is provided. '
             'The default shown here is just an example.');
+    argParser.addOption('build-id',
+        defaultsTo:
+            io.Platform.environment['CIRRUS_BUILD_ID'] ?? 'unknown_build',
+        help:
+            'Optional string to append to the results path, to avoid conflicts. '
+            r'Defaults to $CIRRUS_BUILD_ID if that is set.');
     argParser.addMultiOption('device',
         splitCommas: false,
         defaultsTo: <String>[
@@ -66,38 +74,43 @@
 
   final Print _print;
 
-  Completer<void> _firebaseProjectConfigured;
+  Completer<void>? _firebaseProjectConfigured;
 
   Future<void> _configureFirebaseProject() async {
     if (_firebaseProjectConfigured != null) {
-      return _firebaseProjectConfigured.future;
-    } else {
-      _firebaseProjectConfigured = Completer<void>();
+      return _firebaseProjectConfigured!.future;
     }
-    await processRunner.run(
-      'gcloud',
-      <String>[
-        'auth',
-        'activate-service-account',
-        '--key-file=${getStringArg('service-key')}',
-      ],
-      exitOnError: true,
-      logOnError: true,
-    );
-    final int exitCode = await processRunner.runAndStream('gcloud', <String>[
-      'config',
-      'set',
-      'project',
-      getStringArg('project'),
-    ]);
-    if (exitCode == 0) {
-      _print('\nFirebase project configured.');
-      return;
+    _firebaseProjectConfigured = Completer<void>();
+
+    final String serviceKey = getStringArg('service-key');
+    if (serviceKey.isEmpty) {
+      _print('No --service-key provided; skipping gcloud authorization');
     } else {
-      _print(
-          '\nWarning: gcloud config set returned a non-zero exit code. Continuing anyway.');
+      await processRunner.run(
+        'gcloud',
+        <String>[
+          'auth',
+          'activate-service-account',
+          '--key-file=$serviceKey',
+        ],
+        exitOnError: true,
+        logOnError: true,
+      );
+      final int exitCode = await processRunner.runAndStream('gcloud', <String>[
+        'config',
+        'set',
+        'project',
+        getStringArg('project'),
+      ]);
+      if (exitCode == 0) {
+        _print('\nFirebase project configured.');
+        return;
+      } else {
+        _print(
+            '\nWarning: gcloud config set returned a non-zero exit code. Continuing anyway.');
+      }
     }
-    _firebaseProjectConfigured.complete(null);
+    _firebaseProjectConfigured!.complete(null);
   }
 
   @override
@@ -212,7 +225,7 @@
             failingPackages.add(packageName);
             continue;
           }
-          final String buildId = io.Platform.environment['CIRRUS_BUILD_ID'];
+          final String buildId = getStringArg('build-id');
           final String testRunId = getStringArg('test-run-id');
           final String resultsDir =
               'plugins_android_test/$packageName/$buildId/$testRunId/${resultsCounter++}/';
diff --git a/script/tool/lib/src/lint_podspecs_command.dart b/script/tool/lib/src/lint_podspecs_command.dart
index 72bb6af..364653b 100644
--- a/script/tool/lib/src/lint_podspecs_command.dart
+++ b/script/tool/lib/src/lint_podspecs_command.dart
@@ -2,9 +2,6 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-// @dart=2.9
-
-import 'dart:async';
 import 'dart:convert';
 import 'dart:io';
 
@@ -122,7 +119,7 @@
   }
 
   Future<ProcessResult> _runPodLint(String podspecPath,
-      {bool libraryLint}) async {
+      {required bool libraryLint}) async {
     final bool allowWarnings = (getStringListArg('ignore-warnings'))
         .contains(p.basenameWithoutExtension(podspecPath));
     final List<String> arguments = <String>[
diff --git a/script/tool/test/create_all_plugins_app_command_test.dart b/script/tool/test/create_all_plugins_app_command_test.dart
index b3cbd59..5bde5e0 100644
--- a/script/tool/test/create_all_plugins_app_command_test.dart
+++ b/script/tool/test/create_all_plugins_app_command_test.dart
@@ -2,8 +2,6 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-// @dart=2.9
-
 import 'package:args/command_runner.dart';
 import 'package:file/file.dart';
 import 'package:file/local.dart';
@@ -14,11 +12,11 @@
 
 void main() {
   group('$CreateAllPluginsAppCommand', () {
-    CommandRunner<void> runner;
+    late CommandRunner<void> runner;
     FileSystem fileSystem;
-    Directory testRoot;
-    Directory packagesDir;
-    Directory appDir;
+    late Directory testRoot;
+    late Directory packagesDir;
+    late Directory appDir;
 
     setUp(() {
       // Since the core of this command is a call to 'flutter create', the test
diff --git a/script/tool/test/firebase_test_lab_test.dart b/script/tool/test/firebase_test_lab_test.dart
index 7480900..aa8be17 100644
--- a/script/tool/test/firebase_test_lab_test.dart
+++ b/script/tool/test/firebase_test_lab_test.dart
@@ -2,8 +2,6 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-// @dart=2.9
-
 import 'dart:io';
 
 import 'package:args/command_runner.dart';
@@ -19,10 +17,10 @@
 void main() {
   group('$FirebaseTestLabCommand', () {
     FileSystem fileSystem;
-    Directory packagesDir;
-    List<String> printedMessages;
-    CommandRunner<void> runner;
-    RecordingProcessRunner processRunner;
+    late Directory packagesDir;
+    late List<String> printedMessages;
+    late CommandRunner<void> runner;
+    late RecordingProcessRunner processRunner;
 
     setUp(() {
       fileSystem = MemoryFileSystem();
@@ -31,7 +29,7 @@
       processRunner = RecordingProcessRunner();
       final FirebaseTestLabCommand command = FirebaseTestLabCommand(packagesDir,
           processRunner: processRunner,
-          print: (Object message) => printedMessages.add(message.toString()));
+          print: (Object? message) => printedMessages.add(message.toString()));
 
       runner = CommandRunner<void>(
           'firebase_test_lab_command', 'Test for $FirebaseTestLabCommand');
@@ -97,6 +95,8 @@
         'model=seoul,version=26',
         '--test-run-id',
         'testRunId',
+        '--build-id',
+        'buildId',
       ]);
 
       expect(
@@ -130,7 +130,7 @@
               '/packages/plugin/example/android'),
           ProcessCall(
               'gcloud',
-              'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/0/ --device model=flame,version=29 --device model=seoul,version=26'
+              'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/0/ --device model=flame,version=29 --device model=seoul,version=26'
                   .split(' '),
               '/packages/plugin/example'),
           ProcessCall(
@@ -140,7 +140,7 @@
               '/packages/plugin/example/android'),
           ProcessCall(
               'gcloud',
-              'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/1/ --device model=flame,version=29 --device model=seoul,version=26'
+              'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/1/ --device model=flame,version=29 --device model=seoul,version=26'
                   .split(' '),
               '/packages/plugin/example'),
           ProcessCall(
@@ -150,7 +150,7 @@
               '/packages/plugin/example/android'),
           ProcessCall(
               'gcloud',
-              'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/2/ --device model=flame,version=29 --device model=seoul,version=26'
+              'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/2/ --device model=flame,version=29 --device model=seoul,version=26'
                   .split(' '),
               '/packages/plugin/example'),
           ProcessCall(
@@ -160,7 +160,7 @@
               '/packages/plugin/example/android'),
           ProcessCall(
               'gcloud',
-              'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/3/ --device model=flame,version=29 --device model=seoul,version=26'
+              'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/3/ --device model=flame,version=29 --device model=seoul,version=26'
                   .split(' '),
               '/packages/plugin/example'),
         ]),
@@ -196,6 +196,8 @@
         'model=flame,version=29',
         '--test-run-id',
         'testRunId',
+        '--build-id',
+        'buildId',
         '--enable-experiment=exp1',
       ]);
 
@@ -221,7 +223,7 @@
               '/packages/plugin/example/android'),
           ProcessCall(
               'gcloud',
-              'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/0/ --device model=flame,version=29'
+              'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/0/ --device model=flame,version=29'
                   .split(' '),
               '/packages/plugin/example'),
           ProcessCall(
@@ -231,7 +233,7 @@
               '/packages/plugin/example/android'),
           ProcessCall(
               'gcloud',
-              'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/1/ --device model=flame,version=29'
+              'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/1/ --device model=flame,version=29'
                   .split(' '),
               '/packages/plugin/example'),
           ProcessCall(
@@ -241,7 +243,7 @@
               '/packages/plugin/example/android'),
           ProcessCall(
               'gcloud',
-              'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/2/ --device model=flame,version=29'
+              'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/2/ --device model=flame,version=29'
                   .split(' '),
               '/packages/plugin/example'),
           ProcessCall(
@@ -251,7 +253,7 @@
               '/packages/plugin/example/android'),
           ProcessCall(
               'gcloud',
-              'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/null/testRunId/3/ --device model=flame,version=29'
+              'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/3/ --device model=flame,version=29'
                   .split(' '),
               '/packages/plugin/example'),
         ]),
diff --git a/script/tool/test/lint_podspecs_command_test.dart b/script/tool/test/lint_podspecs_command_test.dart
index 349607b..0183704 100644
--- a/script/tool/test/lint_podspecs_command_test.dart
+++ b/script/tool/test/lint_podspecs_command_test.dart
@@ -2,15 +2,11 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-// @dart=2.9
-
 import 'package:args/command_runner.dart';
 import 'package:file/file.dart';
 import 'package:file/memory.dart';
 import 'package:flutter_plugin_tools/src/lint_podspecs_command.dart';
-import 'package:mockito/mockito.dart';
 import 'package:path/path.dart' as p;
-import 'package:platform/platform.dart';
 import 'package:test/test.dart';
 
 import 'mocks.dart';
@@ -19,24 +15,24 @@
 void main() {
   group('$LintPodspecsCommand', () {
     FileSystem fileSystem;
-    Directory packagesDir;
-    CommandRunner<void> runner;
-    MockPlatform mockPlatform;
-    final RecordingProcessRunner processRunner = RecordingProcessRunner();
-    List<String> printedMessages;
+    late Directory packagesDir;
+    late CommandRunner<void> runner;
+    late MockPlatform mockPlatform;
+    late RecordingProcessRunner processRunner;
+    late List<String> printedMessages;
 
     setUp(() {
       fileSystem = MemoryFileSystem();
       packagesDir = createPackagesDirectory(fileSystem: fileSystem);
 
       printedMessages = <String>[];
-      mockPlatform = MockPlatform();
-      when(mockPlatform.isMacOS).thenReturn(true);
+      mockPlatform = MockPlatform(isMacOS: true);
+      processRunner = RecordingProcessRunner();
       final LintPodspecsCommand command = LintPodspecsCommand(
         packagesDir,
         processRunner: processRunner,
         platform: mockPlatform,
-        print: (Object message) => printedMessages.add(message.toString()),
+        print: (Object? message) => printedMessages.add(message.toString()),
       );
 
       runner =
@@ -53,7 +49,7 @@
         <String>['plugin1.podspec'],
       ]);
 
-      when(mockPlatform.isMacOS).thenReturn(false);
+      mockPlatform.isMacOS = false;
       await runner.run(<String>['podspecs']);
 
       expect(
@@ -172,5 +168,3 @@
     });
   });
 }
-
-class MockPlatform extends Mock implements Platform {}
diff --git a/script/tool/test/mocks.dart b/script/tool/test/mocks.dart
index 66267ec..ba6a03d 100644
--- a/script/tool/test/mocks.dart
+++ b/script/tool/test/mocks.dart
@@ -7,6 +7,14 @@
 
 import 'package:file/file.dart';
 import 'package:mockito/mockito.dart';
+import 'package:platform/platform.dart';
+
+class MockPlatform extends Mock implements Platform {
+  MockPlatform({this.isMacOS = false});
+
+  @override
+  bool isMacOS;
+}
 
 class MockProcess extends Mock implements io.Process {
   final Completer<int> exitCodeCompleter = Completer<int>();
diff --git a/script/tool/test/util.dart b/script/tool/test/util.dart
index a0a316f..c9d4ed2 100644
--- a/script/tool/test/util.dart
+++ b/script/tool/test/util.dart
@@ -257,13 +257,13 @@
     Encoding stderrEncoding = io.systemEncoding,
   }) async {
     recordedCalls.add(ProcessCall(executable, args, workingDir?.path));
-    io.ProcessResult? result;
 
     final io.Process? process = processToReturn;
-    if (process != null) {
-      result = io.ProcessResult(process.pid, await process.exitCode,
-          resultStdout ?? process.stdout, resultStderr ?? process.stderr);
-    }
+    final io.ProcessResult result = process == null
+        ? io.ProcessResult(1, 1, '', '')
+        : io.ProcessResult(process.pid, await process.exitCode,
+            resultStdout ?? process.stdout, resultStderr ?? process.stderr);
+
     return Future<io.ProcessResult>.value(result);
   }