diff --git a/packages/flutter_tools/lib/src/ios/mac.dart b/packages/flutter_tools/lib/src/ios/mac.dart index e4ded3f5c32..f38737af11d 100644 --- a/packages/flutter_tools/lib/src/ios/mac.dart +++ b/packages/flutter_tools/lib/src/ios/mac.dart @@ -105,6 +105,8 @@ Future buildXcodeProject({ return XcodeBuildResult(success: false); } + await removeFinderExtendedAttributes(app.project.hostAppRoot, processUtils, globals.logger); + final XcodeProjectInfo projectInfo = await globals.xcodeProjectInterpreter.getInfo(app.project.hostAppRoot.path); if (!projectInfo.targets.contains('Runner')) { globals.printError('The Xcode project does not define target "Runner" which is needed by Flutter tooling.'); @@ -405,6 +407,25 @@ Future buildXcodeProject({ } } +/// Extended attributes applied by Finder can cause code signing errors. Remove them. +/// https://developer.apple.com/library/archive/qa/qa1940/_index.html +@visibleForTesting +Future removeFinderExtendedAttributes(Directory iosProjectDirectory, ProcessUtils processUtils, Logger logger) async { + final bool success = await processUtils.exitsHappy( + [ + 'xattr', + '-r', + '-d', + 'com.apple.FinderInfo', + iosProjectDirectory.path, + ] + ); + // Ignore all errors, for example if directory is missing. + if (!success) { + logger.printTrace('Failed to remove xattr com.apple.FinderInfo from ${iosProjectDirectory.path}'); + } +} + Future _runBuildWithRetries(List buildCommands, BuildableIOSApp app) async { int buildRetryDelaySeconds = 1; int remainingTries = 8; diff --git a/packages/flutter_tools/test/general.shard/ios/ios_device_start_nonprebuilt_test.dart b/packages/flutter_tools/test/general.shard/ios/ios_device_start_nonprebuilt_test.dart index e82ed73c580..cba84919487 100644 --- a/packages/flutter_tools/test/general.shard/ios/ios_device_start_nonprebuilt_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/ios_device_start_nonprebuilt_test.dart @@ -22,6 +22,16 @@ import '../../src/common.dart'; import '../../src/context.dart'; import '../../src/fake_process_manager.dart'; +List _xattrArgs(FlutterProject flutterProject) { + return [ + 'xattr', + '-r', + '-d', + 'com.apple.FinderInfo', + flutterProject.ios.hostAppRoot.path, + ]; +} + const List kRunReleaseArgs = [ '/usr/bin/env', 'xcrun', @@ -74,6 +84,7 @@ void main() { final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory); final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter'); + processManager.addCommand(FakeCommand(command: _xattrArgs(flutterProject))); processManager.addCommand(const FakeCommand(command: kRunReleaseArgs)); processManager.addCommand(const FakeCommand(command: [...kRunReleaseArgs, '-showBuildSettings'])); processManager.addCommand(FakeCommand( @@ -123,6 +134,7 @@ void main() { final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory); final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter'); + processManager.addCommand(FakeCommand(command: _xattrArgs(flutterProject))); processManager.addCommand(const FakeCommand(command: kRunReleaseArgs)); // The first showBuildSettings call should timeout. processManager.addCommand( @@ -194,6 +206,7 @@ void main() { final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory); final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter'); + processManager.addCommand(FakeCommand(command: _xattrArgs(flutterProject))); // The first xcrun call should fail with a // concurrent build exception. processManager.addCommand( diff --git a/packages/flutter_tools/test/general.shard/ios/mac_test.dart b/packages/flutter_tools/test/general.shard/ios/mac_test.dart index 7c23577a2e3..5efc05539a0 100644 --- a/packages/flutter_tools/test/general.shard/ios/mac_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/mac_test.dart @@ -5,10 +5,12 @@ import 'dart:async'; import 'package:file/file.dart'; +import 'package:file/memory.dart'; import 'package:flutter_tools/src/artifacts.dart'; import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/io.dart' show ProcessResult; import 'package:flutter_tools/src/base/logger.dart'; +import 'package:flutter_tools/src/base/process.dart'; import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/ios/mac.dart'; import 'package:flutter_tools/src/ios/xcodeproj.dart'; @@ -428,6 +430,46 @@ Exited (sigterm)''', ); }); }); + + group('remove Finder extended attributes', () { + Directory iosProjectDirectory; + setUp(() { + final MemoryFileSystem fs = MemoryFileSystem.test(); + iosProjectDirectory = fs.directory('ios'); + }); + + testWithoutContext('removes xattr', () async { + final FakeProcessManager processManager = FakeProcessManager.list([ + FakeCommand(command: [ + 'xattr', + '-r', + '-d', + 'com.apple.FinderInfo', + iosProjectDirectory.path, + ]) + ]); + + await removeFinderExtendedAttributes(iosProjectDirectory, ProcessUtils(processManager: processManager, logger: logger), logger); + expect(processManager.hasRemainingExpectations, false); + }); + + testWithoutContext('ignores errors', () async { + final FakeProcessManager processManager = FakeProcessManager.list([ + FakeCommand(command: [ + 'xattr', + '-r', + '-d', + 'com.apple.FinderInfo', + iosProjectDirectory.path, + ], exitCode: 1, + ) + ]); + + await removeFinderExtendedAttributes(iosProjectDirectory, ProcessUtils(processManager: processManager, logger: logger), logger); + expect(logger.traceText, contains('Failed to remove xattr com.apple.FinderInfo')); + expect(processManager.hasRemainingExpectations, false); + }); + }); } class MockUsage extends Mock implements Usage {}