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 = '''