Harden macOS build use of Xcode project getInfo (#40375)

- Makes build_macos.dart handle the case where there is only one Xcode
  project in the macos/ directory, but it's not called Runner.xcodeproj
- Makes getInfo throw a tool exit when trying to get project info and it
  can't find a project, since that is a configuration error by the user
  rather than a tool bug.
diff --git a/packages/flutter_tools/lib/src/ios/xcodeproj.dart b/packages/flutter_tools/lib/src/ios/xcodeproj.dart
index 181905f..8723faf 100644
--- a/packages/flutter_tools/lib/src/ios/xcodeproj.dart
+++ b/packages/flutter_tools/lib/src/ios/xcodeproj.dart
@@ -7,6 +7,7 @@
 import 'package:meta/meta.dart';
 
 import '../artifacts.dart';
+import '../base/common.dart';
 import '../base/context.dart';
 import '../base/file_system.dart';
 import '../base/io.dart';
@@ -331,6 +332,10 @@
   }
 
   Future<XcodeProjectInfo> getInfo(String projectPath, {String projectFilename}) async {
+    // The exit code returned by 'xcodebuild -list' when either:
+    // * -project is passed and the given project isn't there, or
+    // * no -project is passed and there isn't a project.
+    const int missingProjectExitCode = 66;
     final RunResult result = await processUtils.run(
       <String>[
         _executable,
@@ -338,8 +343,12 @@
         if (projectFilename != null) ...<String>['-project', projectFilename],
       ],
       throwOnError: true,
+      whiteListFailures: (int c) => c == missingProjectExitCode,
       workingDirectory: projectPath,
     );
+    if (result.exitCode == missingProjectExitCode) {
+      throwToolExit('Unable to get Xcode project information:\n ${result.stderr}');
+    }
     return XcodeProjectInfo.fromXcodeBuildOutput(result.toString());
   }
 }
diff --git a/packages/flutter_tools/lib/src/macos/build_macos.dart b/packages/flutter_tools/lib/src/macos/build_macos.dart
index 0aa94f5..678c14c 100644
--- a/packages/flutter_tools/lib/src/macos/build_macos.dart
+++ b/packages/flutter_tools/lib/src/macos/build_macos.dart
@@ -46,9 +46,13 @@
 
   final Directory xcodeProject = flutterProject.macos.xcodeProject;
 
+  // If the standard project exists, specify it to getInfo to handle the case where there are
+  // other Xcode projects in the macos/ directory. Otherwise pass no name, which will work
+  // regardless of the project name so long as there is exactly one project.
+  final String xcodeProjectName = xcodeProject.existsSync() ? xcodeProject.basename : null;
   final XcodeProjectInfo projectInfo = await xcodeProjectInterpreter.getInfo(
     xcodeProject.parent.path,
-    projectFilename: xcodeProject.basename,
+    projectFilename: xcodeProjectName,
   );
   final String scheme = projectInfo.schemeFor(buildInfo);
   if (scheme == null) {
diff --git a/packages/flutter_tools/test/general.shard/ios/xcodeproj_test.dart b/packages/flutter_tools/test/general.shard/ios/xcodeproj_test.dart
index 6870c50..1233b5f 100644
--- a/packages/flutter_tools/test/general.shard/ios/xcodeproj_test.dart
+++ b/packages/flutter_tools/test/general.shard/ios/xcodeproj_test.dart
@@ -173,6 +173,53 @@
       ProcessManager: () => mockProcessManager,
     });
   });
+
+  group('xcodebuild -list', () {
+    mocks.MockProcessManager mockProcessManager;
+    FakePlatform macOS;
+    FileSystem fs;
+
+    setUp(() {
+      mockProcessManager = mocks.MockProcessManager();
+      macOS = fakePlatform('macos');
+      fs = MemoryFileSystem();
+      fs.file(xcodebuild).createSync(recursive: true);
+    });
+
+    void testUsingOsxContext(String description, dynamic testMethod()) {
+      testUsingContext(description, testMethod, overrides: <Type, Generator>{
+        ProcessManager: () => mockProcessManager,
+        Platform: () => macOS,
+        FileSystem: () => fs,
+      });
+    }
+
+    testUsingOsxContext('getInfo returns something when xcodebuild -list succeeds', () async {
+      const String workingDirectory = '/';
+      when(mockProcessManager.run(<String>[xcodebuild, '-list'],
+          environment: anyNamed('environment'),
+          workingDirectory: workingDirectory)).thenAnswer((_) {
+        return Future<ProcessResult>.value(ProcessResult(1, 0, '', ''));
+      });
+      final XcodeProjectInterpreter xcodeProjectInterpreter = XcodeProjectInterpreter();
+      expect(await xcodeProjectInterpreter.getInfo(workingDirectory), isNotNull);
+    });
+
+    testUsingOsxContext('getInfo throws a tool exit when it is unable to find a project', () async {
+      const String workingDirectory = '/';
+      const String stderr = 'Useful Xcode failure message about missing project.';
+      when(mockProcessManager.run(<String>[xcodebuild, '-list'],
+          environment: anyNamed('environment'),
+          workingDirectory: workingDirectory)).thenAnswer((_) {
+        return Future<ProcessResult>.value(ProcessResult(1, 66, '', stderr));
+      });
+      final XcodeProjectInterpreter xcodeProjectInterpreter = XcodeProjectInterpreter();
+      expect(
+          () async => await xcodeProjectInterpreter.getInfo(workingDirectory),
+          throwsToolExit(message: stderr));
+    });
+  });
+
   group('Xcode project properties', () {
     test('properties from default project can be parsed', () {
       const String output = '''