Pass environment variables through to xcodebuild (#43553)

diff --git a/packages/flutter_tools/lib/src/ios/mac.dart b/packages/flutter_tools/lib/src/ios/mac.dart
index e29c1df..d872f19 100644
--- a/packages/flutter_tools/lib/src/ios/mac.dart
+++ b/packages/flutter_tools/lib/src/ios/mac.dart
@@ -464,6 +464,7 @@
   // e.g. `flutter build bundle`.
   buildCommands.add('FLUTTER_SUPPRESS_ANALYTICS=true');
   buildCommands.add('COMPILER_INDEX_STORE_ENABLE=NO');
+  buildCommands.addAll(environmentVariablesAsXcodeBuildSettings());
 
   final Stopwatch sw = Stopwatch()..start();
   initialBuildStatus = logger.startProgress('Running Xcode build...', timeout: timeoutConfiguration.fastOperation);
diff --git a/packages/flutter_tools/lib/src/ios/xcodeproj.dart b/packages/flutter_tools/lib/src/ios/xcodeproj.dart
index 9244719..40bdd7d 100644
--- a/packages/flutter_tools/lib/src/ios/xcodeproj.dart
+++ b/packages/flutter_tools/lib/src/ios/xcodeproj.dart
@@ -293,9 +293,10 @@
       '-target',
       target,
       '-showBuildSettings',
+      ...environmentVariablesAsXcodeBuildSettings()
     ];
     try {
-      // showBuildSettings is reported to ocassionally timeout. Here, we give it
+      // showBuildSettings is reported to occasionally timeout. Here, we give it
       // a lot of wiggle room (locally on Flutter Gallery, this takes ~1s).
       // When there is a timeout, we retry once.
       final RunResult result = await processUtils.run(
@@ -329,6 +330,7 @@
       scheme,
       '-quiet',
       'clean',
+      ...environmentVariablesAsXcodeBuildSettings()
     ], workingDirectory: fs.currentDirectory.path);
   }
 
@@ -354,6 +356,21 @@
   }
 }
 
+/// Environment variables prefixed by FLUTTER_XCODE_ will be passed as build configurations to xcodebuild.
+/// This allows developers to pass arbitrary build settings in without the tool needing to make a flag
+/// for or be aware of each one. This could be used to set code signing build settings in a CI
+/// environment without requiring settings changes in the Xcode project.
+List<String> environmentVariablesAsXcodeBuildSettings() {
+  const String xcodeBuildSettingPrefix = 'FLUTTER_XCODE_';
+  return platform.environment.entries.where((MapEntry<String, String> mapEntry) {
+    return mapEntry.key.startsWith(xcodeBuildSettingPrefix);
+  }).expand<String>((MapEntry<String, String> mapEntry) {
+    // Remove FLUTTER_XCODE_ prefix from the environment variable to get the build setting.
+    final String trimmedBuildSettingKey = mapEntry.key.substring(xcodeBuildSettingPrefix.length);
+    return <String>['$trimmedBuildSettingKey=${mapEntry.value}'];
+  }).toList();
+}
+
 Map<String, String> parseXcodeBuildSettings(String showBuildSettingsOutput) {
   final Map<String, String> settings = <String, String>{};
   for (Match match in showBuildSettingsOutput.split('\n').map<Match>(_settingExpr.firstMatch)) {
diff --git a/packages/flutter_tools/lib/src/macos/build_macos.dart b/packages/flutter_tools/lib/src/macos/build_macos.dart
index 69571bc..d9b16f3 100644
--- a/packages/flutter_tools/lib/src/macos/build_macos.dart
+++ b/packages/flutter_tools/lib/src/macos/build_macos.dart
@@ -79,6 +79,7 @@
       'OBJROOT=${fs.path.join(flutterBuildDir.absolute.path, 'Build', 'Intermediates.noindex')}',
       'SYMROOT=${fs.path.join(flutterBuildDir.absolute.path, 'Build', 'Products')}',
       'COMPILER_INDEX_STORE_ENABLE=NO',
+      ...environmentVariablesAsXcodeBuildSettings()
     ], trace: true);
   } finally {
     status.cancel();
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 b362ca0..bbaf067 100644
--- a/packages/flutter_tools/test/general.shard/ios/xcodeproj_test.dart
+++ b/packages/flutter_tools/test/general.shard/ios/xcodeproj_test.dart
@@ -23,7 +23,7 @@
 const String xcodebuild = '/usr/bin/xcodebuild';
 
 void main() {
-  group('xcodebuild versioning', () {
+  group('xcodebuild commands', () {
     mocks.MockProcessManager mockProcessManager;
     XcodeProjectInterpreter xcodeProjectInterpreter;
     FakePlatform macOS;
@@ -172,6 +172,54 @@
       FileSystem: () => fs,
       ProcessManager: () => mockProcessManager,
     });
+
+    testUsingOsxContext('build settings contains Flutter Xcode environment variables', () async {
+      macOS.environment = Map<String, String>.unmodifiable(<String, String>{
+        'FLUTTER_XCODE_CODE_SIGN_STYLE': 'Manual',
+        'FLUTTER_XCODE_ARCHS': 'arm64'
+      });
+      when(mockProcessManager.runSync(<String>[
+        xcodebuild,
+        '-project',
+        macOS.pathSeparator,
+        '-target',
+        '',
+        '-showBuildSettings',
+        'CODE_SIGN_STYLE=Manual',
+        'ARCHS=arm64'
+      ],
+        workingDirectory: anyNamed('workingDirectory'),
+        environment: anyNamed('environment')))
+        .thenReturn(ProcessResult(1, 0, '', ''));
+      expect(await xcodeProjectInterpreter.getBuildSettings('', ''), const <String, String>{});
+    });
+
+    testUsingOsxContext('clean contains Flutter Xcode environment variables', () async {
+      macOS.environment = Map<String, String>.unmodifiable(<String, String>{
+        'FLUTTER_XCODE_CODE_SIGN_STYLE': 'Manual',
+        'FLUTTER_XCODE_ARCHS': 'arm64'
+      });
+      when(mockProcessManager.runSync(
+        any,
+        workingDirectory: anyNamed('workingDirectory')))
+        .thenReturn(ProcessResult(1, 0, '', ''));
+      xcodeProjectInterpreter.cleanWorkspace('workspace_path', 'Runner');
+      final List<dynamic> captured = verify(mockProcessManager.runSync(
+        captureAny,
+        workingDirectory: anyNamed('workingDirectory'),
+        environment: anyNamed('environment'))).captured;
+      expect(captured.first, <String>[
+        xcodebuild,
+        '-workspace',
+        'workspace_path',
+        '-scheme',
+        'Runner',
+        '-quiet',
+        'clean',
+        'CODE_SIGN_STYLE=Manual',
+        'ARCHS=arm64'
+      ]);
+    });
   });
 
   group('xcodebuild -list', () {
@@ -338,6 +386,27 @@
     });
   });
 
+  group('environmentVariablesAsXcodeBuildSettings', () {
+    FakePlatform platform;
+
+    setUp(() {
+      platform = fakePlatform('ignored');
+    });
+
+    testUsingContext('environment variables as Xcode build settings', () {
+      platform.environment = Map<String, String>.unmodifiable(<String, String>{
+        'Ignored': 'Bogus',
+        'FLUTTER_NOT_XCODE': 'Bogus',
+        'FLUTTER_XCODE_CODE_SIGN_STYLE': 'Manual',
+        'FLUTTER_XCODE_ARCHS': 'arm64'
+      });
+      final List<String> environmentVariablesAsBuildSettings = environmentVariablesAsXcodeBuildSettings();
+      expect(environmentVariablesAsBuildSettings, <String>['CODE_SIGN_STYLE=Manual', 'ARCHS=arm64']);
+    }, overrides: <Type, Generator>{
+      Platform: () => platform
+    });
+  });
+
   group('updateGeneratedXcodeProperties', () {
     MockLocalEngineArtifacts mockArtifacts;
     MockProcessManager mockProcessManager;