// Copyright 2016 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:async'; import 'dart:convert' show json; import 'package:meta/meta.dart'; import '../application_package.dart'; import '../base/common.dart'; import '../base/context.dart'; import '../base/file_system.dart'; import '../base/fingerprint.dart'; import '../base/io.dart'; import '../base/logger.dart'; import '../base/os.dart'; import '../base/platform.dart'; import '../base/process.dart'; import '../base/process_manager.dart'; import '../base/utils.dart'; import '../build_info.dart'; import '../globals.dart'; import '../plugins.dart'; import '../project.dart'; import '../services.dart'; import 'cocoapods.dart'; import 'code_signing.dart'; import 'xcodeproj.dart'; const int kXcodeRequiredVersionMajor = 9; const int kXcodeRequiredVersionMinor = 0; IMobileDevice get iMobileDevice => context[IMobileDevice]; PlistBuddy get plistBuddy => context[PlistBuddy]; Xcode get xcode => context[Xcode]; class PlistBuddy { const PlistBuddy(); static const String path = '/usr/libexec/PlistBuddy'; Future<ProcessResult> run(List<String> args) => processManager.run(<String>[path]..addAll(args)); } /// A property list is a key-value representation commonly used for /// configuration on macOS/iOS systems. class PropertyList { const PropertyList(this.plistPath); final String plistPath; /// Prints the specified key, or returns null if not present. Future<String> read(String key) async { final ProcessResult result = await _runCommand('Print $key'); if (result.exitCode == 0) return result.stdout.trim(); return null; } /// Adds [key]. Has no effect if the key already exists. Future<void> addString(String key, String value) async { await _runCommand('Add $key string $value'); } /// Updates [key] with the new [value]. Has no effect if the key does not exist. Future<void> update(String key, String value) async { await _runCommand('Set $key $value'); } /// Deletes [key]. Future<void> delete(String key) async { await _runCommand('Delete $key'); } /// Deletes the content of the property list and creates a new root of the specified type. Future<void> clearToDict() async { await _runCommand('Clear dict'); } Future<ProcessResult> _runCommand(String command) async { return await plistBuddy.run(<String>['-c', command, plistPath]); } } class IMobileDevice { const IMobileDevice(); bool get isInstalled => exitsHappy(<String>['idevice_id', '-h']); /// Returns true if libimobiledevice is installed and working as expected. /// /// Older releases of libimobiledevice fail to work with iOS 10.3 and above. Future<bool> get isWorking async { if (!isInstalled) return false; // If no device is attached, we're unable to detect any problems. Assume all is well. final ProcessResult result = (await runAsync(<String>['idevice_id', '-l'])).processResult; if (result.exitCode != 0 || result.stdout.isEmpty) return true; // Check that we can look up the names of any attached devices. return await exitsHappyAsync(<String>['idevicename']); } Future<String> getAvailableDeviceIDs() async { try { final ProcessResult result = await processManager.run(<String>['idevice_id', '-l']); if (result.exitCode != 0) throw ToolExit('idevice_id returned an error:\n${result.stderr}'); return result.stdout; } on ProcessException { throw ToolExit('Failed to invoke idevice_id. Run flutter doctor.'); } } Future<String> getInfoForDevice(String deviceID, String key) async { try { final ProcessResult result = await processManager.run(<String>['ideviceinfo', '-u', deviceID, '-k', key, '--simple']); if (result.exitCode != 0) throw ToolExit('idevice_id returned an error:\n${result.stderr}'); return result.stdout.trim(); } on ProcessException { throw ToolExit('Failed to invoke idevice_id. Run flutter doctor.'); } } /// Starts `idevicesyslog` and returns the running process. Future<Process> startLogger() => runCommand(<String>['idevicesyslog']); /// Captures a screenshot to the specified outputFile. Future<void> takeScreenshot(File outputFile) { return runCheckedAsync(<String>['idevicescreenshot', outputFile.path]); } } class Xcode { bool get isInstalledAndMeetsVersionCheck => isInstalled && isVersionSatisfactory; String _xcodeSelectPath; String get xcodeSelectPath { if (_xcodeSelectPath == null) { try { _xcodeSelectPath = processManager.runSync(<String>['/usr/bin/xcode-select', '--print-path']).stdout.trim(); } on ProcessException { // Ignore: return null below. } } return _xcodeSelectPath; } bool get isInstalled { if (xcodeSelectPath == null || xcodeSelectPath.isEmpty) return false; return xcodeProjectInterpreter.isInstalled; } int get majorVersion => xcodeProjectInterpreter.majorVersion; int get minorVersion => xcodeProjectInterpreter.minorVersion; String get versionText => xcodeProjectInterpreter.versionText; bool _eulaSigned; /// Has the EULA been signed? bool get eulaSigned { if (_eulaSigned == null) { try { final ProcessResult result = processManager.runSync(<String>['/usr/bin/xcrun', 'clang']); if (result.stdout != null && result.stdout.contains('license')) _eulaSigned = false; else if (result.stderr != null && result.stderr.contains('license')) _eulaSigned = false; else _eulaSigned = true; } on ProcessException { _eulaSigned = false; } } return _eulaSigned; } bool _isSimctlInstalled; /// Verifies that simctl is installed by trying to run it. bool get isSimctlInstalled { if (_isSimctlInstalled == null) { try { // This command will error if additional components need to be installed in // xcode 9.2 and above. final ProcessResult result = processManager.runSync(<String>['/usr/bin/xcrun', 'simctl', 'list']); _isSimctlInstalled = result.stderr == null || result.stderr == ''; } on ProcessException { _isSimctlInstalled = false; } } return _isSimctlInstalled; } bool get isVersionSatisfactory { if (!xcodeProjectInterpreter.isInstalled) return false; if (majorVersion > kXcodeRequiredVersionMajor) return true; if (majorVersion == kXcodeRequiredVersionMajor) return minorVersion >= kXcodeRequiredVersionMinor; return false; } Future<RunResult> cc(List<String> args) { return runCheckedAsync(<String>['xcrun', 'cc']..addAll(args)); } Future<RunResult> clang(List<String> args) { return runCheckedAsync(<String>['xcrun', 'clang']..addAll(args)); } String getSimulatorPath() { if (xcodeSelectPath == null) return null; final List<String> searchPaths = <String>[ fs.path.join(xcodeSelectPath, 'Applications', 'Simulator.app'), ]; return searchPaths.where((String p) => p != null).firstWhere( (String p) => fs.directory(p).existsSync(), orElse: () => null, ); } } /// Sets the Xcode system. /// /// Xcode 10 added a new (default) build system with better performance and /// stricter checks. Flutter apps without plugins build fine under the new /// system, but it causes build breakages in projects with CocoaPods enabled. /// This affects Flutter apps with plugins. /// /// Once Flutter has been updated to be fully compliant with the new build /// system, this can be removed. // // TODO(cbracken): remove when https://github.com/flutter/flutter/issues/20685 is fixed. Future<void> setXcodeWorkspaceBuildSystem({ @required Directory workspaceDirectory, @required File workspaceSettings, @required bool modern, }) async { // If this isn't a workspace, we're not using CocoaPods and can use the new // build system. if (!workspaceDirectory.existsSync()) return; final PropertyList plist = PropertyList(workspaceSettings.path); if (!workspaceSettings.existsSync()) { workspaceSettings.parent.createSync(recursive: true); await plist.clearToDict(); } const String kBuildSystemType = 'BuildSystemType'; if (modern) { printTrace('Using new Xcode build system.'); await plist.delete(kBuildSystemType); } else { printTrace('Using legacy Xcode build system.'); if (await plist.read(kBuildSystemType) == null) { await plist.addString(kBuildSystemType, 'Original'); } else { await plist.update(kBuildSystemType, 'Original'); } } } Future<XcodeBuildResult> buildXcodeProject({ BuildableIOSApp app, BuildInfo buildInfo, String targetOverride, bool buildForDevice, bool codesign = true, bool usesTerminalUi = true, }) async { if (!await upgradePbxProjWithFlutterAssets(app.project)) return XcodeBuildResult(success: false); if (!_checkXcodeVersion()) return XcodeBuildResult(success: false); // TODO(cbracken) remove when https://github.com/flutter/flutter/issues/20685 is fixed. await setXcodeWorkspaceBuildSystem( workspaceDirectory: app.project.xcodeWorkspace, workspaceSettings: app.project.xcodeWorkspaceSharedSettings, modern: false, ); final XcodeProjectInfo projectInfo = xcodeProjectInterpreter.getInfo(app.project.hostAppRoot.path); if (!projectInfo.targets.contains('Runner')) { printError('The Xcode project does not define target "Runner" which is needed by Flutter tooling.'); printError('Open Xcode to fix the problem:'); printError(' open ios/Runner.xcworkspace'); return XcodeBuildResult(success: false); } final String scheme = projectInfo.schemeFor(buildInfo); if (scheme == null) { printError(''); if (projectInfo.definesCustomSchemes) { printError('The Xcode project defines schemes: ${projectInfo.schemes.join(', ')}'); printError('You must specify a --flavor option to select one of them.'); } else { printError('The Xcode project does not define custom schemes.'); printError('You cannot use the --flavor option.'); } return XcodeBuildResult(success: false); } final String configuration = projectInfo.buildConfigurationFor(buildInfo, scheme); if (configuration == null) { printError(''); printError('The Xcode project defines build configurations: ${projectInfo.buildConfigurations.join(', ')}'); printError('Flutter expects a build configuration named ${XcodeProjectInfo.expectedBuildConfigurationFor(buildInfo, scheme)} or similar.'); printError('Open Xcode to fix the problem:'); printError(' open ios/Runner.xcworkspace'); return XcodeBuildResult(success: false); } Map<String, String> autoSigningConfigs; if (codesign && buildForDevice) autoSigningConfigs = await getCodeSigningIdentityDevelopmentTeam(iosApp: app, usesTerminalUi: usesTerminalUi); // Before the build, all service definitions must be updated and the dylibs // copied over to a location that is suitable for Xcodebuild to find them. await _addServicesToBundle(app.project.hostAppRoot); final FlutterProject project = await FlutterProject.current(); await updateGeneratedXcodeProperties( project: project, targetOverride: targetOverride, buildInfo: buildInfo, ); refreshPluginsList(project); if (hasPlugins(project) || (project.isModule && project.ios.podfile.existsSync())) { // If the Xcode project, Podfile, or Generated.xcconfig have changed since // last run, pods should be updated. final Fingerprinter fingerprinter = Fingerprinter( fingerprintPath: fs.path.join(getIosBuildDirectory(), 'pod_inputs.fingerprint'), paths: <String>[ app.project.xcodeProjectInfoFile.path, app.project.podfile.path, app.project.generatedXcodePropertiesFile.path, ], properties: <String, String>{}, ); final bool didPodInstall = await cocoaPods.processPods( iosProject: project.ios, iosEngineDir: flutterFrameworkDir(buildInfo.mode), isSwift: project.ios.isSwift, dependenciesChanged: !await fingerprinter.doesFingerprintMatch() ); if (didPodInstall) await fingerprinter.writeFingerprint(); } final List<String> buildCommands = <String>[ '/usr/bin/env', 'xcrun', 'xcodebuild', '-configuration', configuration, ]; if (logger.isVerbose) { // An environment variable to be passed to xcode_backend.sh determining // whether to echo back executed commands. buildCommands.add('VERBOSE_SCRIPT_LOGGING=YES'); } else { // This will print warnings and errors only. buildCommands.add('-quiet'); } if (autoSigningConfigs != null) { for (MapEntry<String, String> signingConfig in autoSigningConfigs.entries) { buildCommands.add('${signingConfig.key}=${signingConfig.value}'); } buildCommands.add('-allowProvisioningUpdates'); buildCommands.add('-allowProvisioningDeviceRegistration'); } final List<FileSystemEntity> contents = app.project.hostAppRoot.listSync(); for (FileSystemEntity entity in contents) { if (fs.path.extension(entity.path) == '.xcworkspace') { buildCommands.addAll(<String>[ '-workspace', fs.path.basename(entity.path), '-scheme', scheme, 'BUILD_DIR=${fs.path.absolute(getIosBuildDirectory())}', ]); break; } } if (buildForDevice) { buildCommands.addAll(<String>['-sdk', 'iphoneos']); } else { buildCommands.addAll(<String>['-sdk', 'iphonesimulator', '-arch', 'x86_64']); } if (!codesign) { buildCommands.addAll( <String>[ 'CODE_SIGNING_ALLOWED=NO', 'CODE_SIGNING_REQUIRED=NO', 'CODE_SIGNING_IDENTITY=""' ] ); } Status buildSubStatus; Status initialBuildStatus; Directory tempDir; if (logger.hasTerminal) { tempDir = fs.systemTempDirectory.createTempSync('flutter_build_log_pipe.'); final File scriptOutputPipeFile = tempDir.childFile('pipe_to_stdout'); os.makePipe(scriptOutputPipeFile.path); Future<void> listenToScriptOutputLine() async { final List<String> lines = await scriptOutputPipeFile.readAsLines(); for (String line in lines) { if (line == 'done') { buildSubStatus?.stop(); buildSubStatus = null; } else { initialBuildStatus.cancel(); buildSubStatus = logger.startProgress( line, expectSlowOperation: true, progressIndicatorPadding: kDefaultStatusPadding - 7, ); } } return listenToScriptOutputLine(); } // Trigger the start of the pipe -> stdout loop. Ignore exceptions. listenToScriptOutputLine(); // ignore: unawaited_futures buildCommands.add('SCRIPT_OUTPUT_STREAM_FILE=${scriptOutputPipeFile.absolute.path}'); } final Stopwatch buildStopwatch = Stopwatch()..start(); initialBuildStatus = logger.startProgress('Starting Xcode build...'); final RunResult buildResult = await runAsync( buildCommands, workingDirectory: app.project.hostAppRoot.path, allowReentrantFlutter: true ); buildSubStatus?.stop(); initialBuildStatus?.cancel(); buildStopwatch.stop(); // Free pipe file. tempDir?.deleteSync(recursive: true); printStatus( 'Xcode build done.'.padRight(kDefaultStatusPadding + 1) + '${getElapsedAsSeconds(buildStopwatch.elapsed).padLeft(5)}', ); // Run -showBuildSettings again but with the exact same parameters as the build. final Map<String, String> buildSettings = parseXcodeBuildSettings(runCheckedSync( (List<String> .from(buildCommands) ..add('-showBuildSettings')) // Undocumented behaviour: xcodebuild craps out if -showBuildSettings // is used together with -allowProvisioningUpdates or // -allowProvisioningDeviceRegistration and freezes forever. .where((String buildCommand) { return !const <String>[ '-allowProvisioningUpdates', '-allowProvisioningDeviceRegistration', ].contains(buildCommand); }).toList(), workingDirectory: app.project.hostAppRoot.path, )); if (buildResult.exitCode != 0) { printStatus('Failed to build iOS app'); if (buildResult.stderr.isNotEmpty) { printStatus('Error output from Xcode build:\n↳'); printStatus(buildResult.stderr, indent: 4); } if (buildResult.stdout.isNotEmpty) { printStatus('Xcode\'s output:\n↳'); printStatus(buildResult.stdout, indent: 4); } return XcodeBuildResult( success: false, stdout: buildResult.stdout, stderr: buildResult.stderr, xcodeBuildExecution: XcodeBuildExecution( buildCommands: buildCommands, appDirectory: app.project.hostAppRoot.path, buildForPhysicalDevice: buildForDevice, buildSettings: buildSettings, ), ); } else { final String expectedOutputDirectory = fs.path.join( buildSettings['TARGET_BUILD_DIR'], buildSettings['WRAPPER_NAME'], ); String outputDir; if (fs.isDirectorySync(expectedOutputDirectory)) { // Copy app folder to a place where other tools can find it without knowing // the BuildInfo. outputDir = expectedOutputDirectory.replaceFirst('/$configuration-', '/'); if (fs.isDirectorySync(outputDir)) { // Previous output directory might have incompatible artifacts // (for example, kernel binary files produced from previous run). fs.directory(outputDir).deleteSync(recursive: true); } copyDirectorySync(fs.directory(expectedOutputDirectory), fs.directory(outputDir)); } else { printError('Build succeeded but the expected app at $expectedOutputDirectory not found'); } return XcodeBuildResult(success: true, output: outputDir); } } String readGeneratedXcconfig(String appPath) { final String generatedXcconfigPath = fs.path.join(fs.currentDirectory.path, appPath, 'Flutter', 'Generated.xcconfig'); final File generatedXcconfigFile = fs.file(generatedXcconfigPath); if (!generatedXcconfigFile.existsSync()) return null; return generatedXcconfigFile.readAsStringSync(); } Future<Null> diagnoseXcodeBuildFailure(XcodeBuildResult result) async { if (result.xcodeBuildExecution != null && result.xcodeBuildExecution.buildForPhysicalDevice && result.stdout?.contains('BCEROR') == true && // May need updating if Xcode changes its outputs. result.stdout?.contains('Xcode couldn\'t find a provisioning profile matching') == true) { printError(noProvisioningProfileInstruction, emphasis: true); return; } // Make sure the user has specified one of: // * DEVELOPMENT_TEAM (automatic signing) // * PROVISIONING_PROFILE (manual signing) if (result.xcodeBuildExecution != null && result.xcodeBuildExecution.buildForPhysicalDevice && !<String>['DEVELOPMENT_TEAM', 'PROVISIONING_PROFILE'].any( result.xcodeBuildExecution.buildSettings.containsKey) ) { printError(noDevelopmentTeamInstruction, emphasis: true); return; } if (result.xcodeBuildExecution != null && result.xcodeBuildExecution.buildForPhysicalDevice && result.xcodeBuildExecution.buildSettings['PRODUCT_BUNDLE_IDENTIFIER']?.contains('com.example') == true) { printError(''); printError('It appears that your application still contains the default signing identifier.'); printError("Try replacing 'com.example' with your signing id in Xcode:"); printError(' open ios/Runner.xcworkspace'); return; } if (result.stdout?.contains('Code Sign error') == true) { printError(''); printError('It appears that there was a problem signing your application prior to installation on the device.'); printError(''); printError('Verify that the Bundle Identifier in your project is your signing id in Xcode'); printError(' open ios/Runner.xcworkspace'); printError(''); printError("Also try selecting 'Product > Build' to fix the problem:"); return; } } class XcodeBuildResult { XcodeBuildResult( { @required this.success, this.output, this.stdout, this.stderr, this.xcodeBuildExecution, } ); final bool success; final String output; final String stdout; final String stderr; /// The invocation of the build that resulted in this result instance. final XcodeBuildExecution xcodeBuildExecution; } /// Describes an invocation of a Xcode build command. class XcodeBuildExecution { XcodeBuildExecution( { @required this.buildCommands, @required this.appDirectory, @required this.buildForPhysicalDevice, @required this.buildSettings, } ); /// The original list of Xcode build commands used to produce this build result. final List<String> buildCommands; final String appDirectory; final bool buildForPhysicalDevice; /// The build settings corresponding to the [buildCommands] invocation. final Map<String, String> buildSettings; } const String _xcodeRequirement = 'Xcode $kXcodeRequiredVersionMajor.$kXcodeRequiredVersionMinor or greater is required to develop for iOS.'; bool _checkXcodeVersion() { if (!platform.isMacOS) return false; if (!xcodeProjectInterpreter.isInstalled) { printError('Cannot find "xcodebuild". $_xcodeRequirement'); return false; } if (!xcode.isVersionSatisfactory) { printError('Found "${xcodeProjectInterpreter.versionText}". $_xcodeRequirement'); return false; } return true; } Future<Null> _addServicesToBundle(Directory bundle) async { final List<Map<String, String>> services = <Map<String, String>>[]; printTrace('Trying to resolve native pub services.'); // Step 1: Parse the service configuration yaml files present in the service // pub packages. await parseServiceConfigs(services); printTrace('Found ${services.length} service definition(s).'); // Step 2: Copy framework dylibs to the correct spot for xcodebuild to pick up. final Directory frameworksDirectory = fs.directory(fs.path.join(bundle.path, 'Frameworks')); await _copyServiceFrameworks(services, frameworksDirectory); // Step 3: Copy the service definitions manifest at the correct spot for // xcodebuild to pick up. final File manifestFile = fs.file(fs.path.join(bundle.path, 'ServiceDefinitions.json')); _copyServiceDefinitionsManifest(services, manifestFile); } Future<Null> _copyServiceFrameworks(List<Map<String, String>> services, Directory frameworksDirectory) async { printTrace("Copying service frameworks to '${fs.path.absolute(frameworksDirectory.path)}'."); frameworksDirectory.createSync(recursive: true); for (Map<String, String> service in services) { final String dylibPath = await getServiceFromUrl(service['ios-framework'], service['root'], service['name']); final File dylib = fs.file(dylibPath); printTrace('Copying ${dylib.path} into bundle.'); if (!dylib.existsSync()) { printError("The service dylib '${dylib.path}' does not exist."); continue; } // Shell out so permissions on the dylib are preserved. await runCheckedAsync(<String>['/bin/cp', dylib.path, frameworksDirectory.path]); } } void _copyServiceDefinitionsManifest(List<Map<String, String>> services, File manifest) { printTrace("Creating service definitions manifest at '${manifest.path}'"); final List<Map<String, String>> jsonServices = services.map<Map<String, String>>((Map<String, String> service) => <String, String>{ 'name': service['name'], // Since we have already moved it to the Frameworks directory. Strip away // the directory and basenames. 'framework': fs.path.basenameWithoutExtension(service['ios-framework']) }).toList(); final Map<String, dynamic> jsonObject = <String, dynamic>{ 'services' : jsonServices }; manifest.writeAsStringSync(json.encode(jsonObject), mode: FileMode.write, flush: true); } Future<bool> upgradePbxProjWithFlutterAssets(IosProject project) async { final File xcodeProjectFile = project.xcodeProjectInfoFile; assert(await xcodeProjectFile.exists()); final List<String> lines = await xcodeProjectFile.readAsLines(); if (lines.any((String line) => line.contains('flutter_assets in Resources'))) return true; const String l1 = ' 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };'; const String l2 = ' 2D5378261FAA1A9400D5DBA9 /* flutter_assets in Resources */ = {isa = PBXBuildFile; fileRef = 2D5378251FAA1A9400D5DBA9 /* flutter_assets */; };'; const String l3 = ' 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };'; const String l4 = ' 2D5378251FAA1A9400D5DBA9 /* flutter_assets */ = {isa = PBXFileReference; lastKnownFileType = folder; name = flutter_assets; path = Flutter/flutter_assets; sourceTree = SOURCE_ROOT; };'; const String l5 = ' 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,'; const String l6 = ' 2D5378251FAA1A9400D5DBA9 /* flutter_assets */,'; const String l7 = ' 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,'; const String l8 = ' 2D5378261FAA1A9400D5DBA9 /* flutter_assets in Resources */,'; printStatus("Upgrading project.pbxproj of ${project.hostAppBundleName}' to include the " "'flutter_assets' directory"); if (!lines.contains(l1) || !lines.contains(l3) || !lines.contains(l5) || !lines.contains(l7)) { printError('Automatic upgrade of project.pbxproj failed.'); printError(' To manually upgrade, open ${xcodeProjectFile.path}:'); printError(' Add the following line in the "PBXBuildFile" section'); printError(l2); printError(' Add the following line in the "PBXFileReference" section'); printError(l4); printError(' Add the following line in the "children" list of the "Flutter" group in the "PBXGroup" section'); printError(l6); printError(' Add the following line in the "files" list of "Resources" in the "PBXResourcesBuildPhase" section'); printError(l8); return false; } lines.insert(lines.indexOf(l1) + 1, l2); lines.insert(lines.indexOf(l3) + 1, l4); lines.insert(lines.indexOf(l5) + 1, l6); lines.insert(lines.indexOf(l7) + 1, l8); const String l9 = ' 9740EEBB1CF902C7004384FC /* app.flx in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB71CF902C7004384FC /* app.flx */; };'; const String l10 = ' 9740EEB71CF902C7004384FC /* app.flx */ = {isa = PBXFileReference; lastKnownFileType = file; name = app.flx; path = Flutter/app.flx; sourceTree = "<group>"; };'; const String l11 = ' 9740EEB71CF902C7004384FC /* app.flx */,'; const String l12 = ' 9740EEBB1CF902C7004384FC /* app.flx in Resources */,'; if (lines.contains(l9)) { printStatus('Removing app.flx from project.pbxproj since it has been ' 'replaced with flutter_assets.'); lines.remove(l9); lines.remove(l10); lines.remove(l11); lines.remove(l12); } final StringBuffer buffer = StringBuffer(); lines.forEach(buffer.writeln); await xcodeProjectFile.writeAsString(buffer.toString()); return true; }