From 4b12645012342076800eb701bcdfe18f87da21cf Mon Sep 17 00:00:00 2001 From: Jenn Magder Date: Mon, 3 Apr 2023 17:46:48 -0700 Subject: [PATCH] Add readlink -f flag to CocoaPods script to workaround Xcode 14.3 issue (#124079) Cherry-pick https://github.com/flutter/flutter/pull/124062 onto stable. CP request at https://github.com/flutter/flutter/issues/124081 --- .../lib/src/macos/cocoapods.dart | 9 ++ .../migrations/cocoapods_script_symlink.dart | 52 +++++++++ .../flutter_tools/lib/src/xcode_project.dart | 7 ++ .../ios/ios_project_migration_test.dart | 105 ++++++++++++++++++ .../general.shard/macos/cocoapods_test.dart | 50 +++++++++ 5 files changed, 223 insertions(+) create mode 100644 packages/flutter_tools/lib/src/migrations/cocoapods_script_symlink.dart diff --git a/packages/flutter_tools/lib/src/macos/cocoapods.dart b/packages/flutter_tools/lib/src/macos/cocoapods.dart index a131de0fc2a4ff..07cad54696d895 100644 --- a/packages/flutter_tools/lib/src/macos/cocoapods.dart +++ b/packages/flutter_tools/lib/src/macos/cocoapods.dart @@ -13,10 +13,12 @@ import '../base/logger.dart'; import '../base/os.dart'; import '../base/platform.dart'; import '../base/process.dart'; +import '../base/project_migrator.dart'; import '../base/version.dart'; import '../build_info.dart'; import '../cache.dart'; import '../ios/xcodeproj.dart'; +import '../migrations/cocoapods_script_symlink.dart'; import '../reporting/reporting.dart'; import '../xcode_project.dart'; @@ -166,6 +168,13 @@ class CocoaPods { throwToolExit('CocoaPods not installed or not in valid state.'); } await _runPodInstall(xcodeProject, buildMode); + + // This migrator works around a CocoaPods bug, and should be run after `pod install` is run. + final ProjectMigration postPodMigration = ProjectMigration([ + CocoaPodsScriptReadlink(xcodeProject, _xcodeProjectInterpreter, _logger), + ]); + postPodMigration.run(); + podsProcessed = true; } return podsProcessed; diff --git a/packages/flutter_tools/lib/src/migrations/cocoapods_script_symlink.dart b/packages/flutter_tools/lib/src/migrations/cocoapods_script_symlink.dart new file mode 100644 index 00000000000000..f0d90ca0f8044d --- /dev/null +++ b/packages/flutter_tools/lib/src/migrations/cocoapods_script_symlink.dart @@ -0,0 +1,52 @@ +// Copyright 2014 The Flutter 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 '../base/file_system.dart'; +import '../base/project_migrator.dart'; +import '../base/version.dart'; +import '../ios/xcodeproj.dart'; +import '../xcode_project.dart'; + +// Xcode 14.3 changed the readlink symlink behavior to be relative from the script working directory, instead of the +// relative path of the symlink. The -f flag returns the original "--canonicalize" behavior the CocoaPods script relies on. +// This has been fixed upstream in CocoaPods, but migrate a copy of their workaround so users don't need to update. +// +// See https://github.com/flutter/flutter/issues/123890#issuecomment-1494825976. +class CocoaPodsScriptReadlink extends ProjectMigrator { + CocoaPodsScriptReadlink( + XcodeBasedProject project, + XcodeProjectInterpreter xcodeProjectInterpreter, + super.logger, + ) : _podRunnerFrameworksScript = project.podRunnerFrameworksScript, + _xcodeProjectInterpreter = xcodeProjectInterpreter; + + final File _podRunnerFrameworksScript; + final XcodeProjectInterpreter _xcodeProjectInterpreter; + + @override + void migrate() { + if (!_podRunnerFrameworksScript.existsSync()) { + logger.printTrace('CocoaPods Pods-Runner-frameworks.sh script not found, skipping "readlink -f" workaround.'); + return; + } + + final Version? version = _xcodeProjectInterpreter.version; + + // If Xcode not installed or less than 14.3 with readlink behavior change, skip this migration. + if (version == null || version < Version(14, 3, 0)) { + logger.printTrace('Detected Xcode version is $version, below 14.3, skipping "readlink -f" workaround.'); + return; + } + + processFileLines(_podRunnerFrameworksScript); + } + + @override + String? migrateLine(String line) { + const String originalReadLinkLine = r'source="$(readlink "${source}")"'; + const String replacementReadLinkLine = r'source="$(readlink -f "${source}")"'; + + return line.replaceAll(originalReadLinkLine, replacementReadLinkLine); + } +} diff --git a/packages/flutter_tools/lib/src/xcode_project.dart b/packages/flutter_tools/lib/src/xcode_project.dart index ca08f8960c48b0..485be406dab4ae 100644 --- a/packages/flutter_tools/lib/src/xcode_project.dart +++ b/packages/flutter_tools/lib/src/xcode_project.dart @@ -89,6 +89,13 @@ abstract class XcodeBasedProject extends FlutterProjectPlatform { /// The CocoaPods 'Manifest.lock'. File get podManifestLock => hostAppRoot.childDirectory('Pods').childFile('Manifest.lock'); + + /// The CocoaPods generated 'Pods-Runner-frameworks.sh'. + File get podRunnerFrameworksScript => hostAppRoot + .childDirectory('Pods') + .childDirectory('Target Support Files') + .childDirectory('Pods-Runner') + .childFile('Pods-Runner-frameworks.sh'); } /// Represents the iOS sub-project of a Flutter project. diff --git a/packages/flutter_tools/test/general.shard/ios/ios_project_migration_test.dart b/packages/flutter_tools/test/general.shard/ios/ios_project_migration_test.dart index 6326e439d7f8ff..beb40cb4b09ce9 100644 --- a/packages/flutter_tools/test/general.shard/ios/ios_project_migration_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/ios_project_migration_test.dart @@ -6,6 +6,7 @@ import 'package:file/file.dart'; import 'package:file/memory.dart'; import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/project_migrator.dart'; +import 'package:flutter_tools/src/base/version.dart'; import 'package:flutter_tools/src/ios/migrations/host_app_info_plist_migration.dart'; import 'package:flutter_tools/src/ios/migrations/ios_deployment_target_migration.dart'; import 'package:flutter_tools/src/ios/migrations/project_base_configuration_migration.dart'; @@ -13,6 +14,8 @@ import 'package:flutter_tools/src/ios/migrations/project_build_location_migratio import 'package:flutter_tools/src/ios/migrations/remove_bitcode_migration.dart'; import 'package:flutter_tools/src/ios/migrations/remove_framework_link_and_embedding_migration.dart'; import 'package:flutter_tools/src/ios/migrations/xcode_build_system_migration.dart'; +import 'package:flutter_tools/src/ios/xcodeproj.dart'; +import 'package:flutter_tools/src/migrations/cocoapods_script_symlink.dart'; import 'package:flutter_tools/src/migrations/xcode_project_object_version_migration.dart'; import 'package:flutter_tools/src/migrations/xcode_script_build_phase_migration.dart'; import 'package:flutter_tools/src/reporting/reporting.dart'; @@ -20,6 +23,7 @@ import 'package:flutter_tools/src/xcode_project.dart'; import 'package:test/fake.dart'; import '../../src/common.dart'; +import '../../src/fake_process_manager.dart'; void main () { group('iOS migration', () { @@ -900,6 +904,104 @@ platform :ios, '11.0' expect('Disabling deprecated bitcode Xcode build setting'.allMatches(testLogger.warningText).length, 1); }); }); + + group('CocoaPods script readlink', () { + late MemoryFileSystem memoryFileSystem; + late BufferLogger testLogger; + late FakeIosProject project; + late File podRunnerFrameworksScript; + late ProcessManager processManager; + late XcodeProjectInterpreter xcode143ProjectInterpreter; + + setUp(() { + memoryFileSystem = MemoryFileSystem(); + podRunnerFrameworksScript = memoryFileSystem.file('Pods-Runner-frameworks.sh'); + testLogger = BufferLogger.test(); + project = FakeIosProject(); + processManager = FakeProcessManager.any(); + xcode143ProjectInterpreter = XcodeProjectInterpreter.test(processManager: processManager, version: Version(14, 3, 0)); + project.podRunnerFrameworksScript = podRunnerFrameworksScript; + }); + + testWithoutContext('skipped if files are missing', () { + final CocoaPodsScriptReadlink iosProjectMigration = CocoaPodsScriptReadlink( + project, + xcode143ProjectInterpreter, + testLogger, + ); + iosProjectMigration.migrate(); + expect(podRunnerFrameworksScript.existsSync(), isFalse); + + expect(testLogger.traceText, contains('CocoaPods Pods-Runner-frameworks.sh script not found')); + expect(testLogger.statusText, isEmpty); + }); + + testWithoutContext('skipped if nothing to upgrade', () { + const String contents = r''' + if [ -L "${source}" ]; then + echo "Symlinked..." + source="$(readlink -f "${source}")" + fi'''; + podRunnerFrameworksScript.writeAsStringSync(contents); + + final CocoaPodsScriptReadlink iosProjectMigration = CocoaPodsScriptReadlink( + project, + xcode143ProjectInterpreter, + testLogger, + ); + iosProjectMigration.migrate(); + expect(podRunnerFrameworksScript.existsSync(), isTrue); + expect(testLogger.traceText, isEmpty); + expect(testLogger.statusText, isEmpty); + }); + + testWithoutContext('skipped if Xcode version below 14.3', () { + const String contents = r''' + if [ -L "${source}" ]; then + echo "Symlinked..." + source="$(readlink "${source}")" + fi'''; + podRunnerFrameworksScript.writeAsStringSync(contents); + + final XcodeProjectInterpreter xcode142ProjectInterpreter = XcodeProjectInterpreter.test( + processManager: processManager, + version: Version(14, 2, 0), + ); + + final CocoaPodsScriptReadlink iosProjectMigration = CocoaPodsScriptReadlink( + project, + xcode142ProjectInterpreter, + testLogger, + ); + iosProjectMigration.migrate(); + expect(podRunnerFrameworksScript.existsSync(), isTrue); + expect(testLogger.traceText, contains('Detected Xcode version is 14.2.0, below 14.3, skipping "readlink -f" workaround')); + expect(testLogger.statusText, isEmpty); + }); + + testWithoutContext('Xcode project is migrated', () { + const String contents = r''' + if [ -L "${source}" ]; then + echo "Symlinked..." + source="$(readlink "${source}")" + fi'''; + podRunnerFrameworksScript.writeAsStringSync(contents); + + final CocoaPodsScriptReadlink iosProjectMigration = CocoaPodsScriptReadlink( + project, + xcode143ProjectInterpreter, + testLogger, + ); + iosProjectMigration.migrate(); + expect(podRunnerFrameworksScript.readAsStringSync(), r''' + if [ -L "${source}" ]; then + echo "Symlinked..." + source="$(readlink -f "${source}")" + fi +'''); + expect(testLogger.statusText, contains('Upgrading Pods-Runner-frameworks.sh')); + }); + }); }); group('update Xcode script build phase', () { @@ -1025,6 +1127,9 @@ class FakeIosProject extends Fake implements IosProject { @override File podfile = MemoryFileSystem.test().file('Podfile'); + + @override + File podRunnerFrameworksScript = MemoryFileSystem.test().file('podRunnerFrameworksScript'); } class FakeIOSMigrator extends ProjectMigrator { diff --git a/packages/flutter_tools/test/general.shard/macos/cocoapods_test.dart b/packages/flutter_tools/test/general.shard/macos/cocoapods_test.dart index 3c34f14426e71b..a03134ef117954 100644 --- a/packages/flutter_tools/test/general.shard/macos/cocoapods_test.dart +++ b/packages/flutter_tools/test/general.shard/macos/cocoapods_test.dart @@ -6,6 +6,7 @@ import 'package:file/file.dart'; import 'package:file/memory.dart'; import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/platform.dart'; +import 'package:flutter_tools/src/base/version.dart'; import 'package:flutter_tools/src/build_info.dart'; import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/flutter_plugins.dart'; @@ -729,6 +730,55 @@ Note: as of CocoaPods 1.0, `pod repo update` does not happen on `pod install` by ); expect(didInstall, isTrue); expect(fakeProcessManager, hasNoRemainingExpectations); + expect(logger.traceText, contains('CocoaPods Pods-Runner-frameworks.sh script not found')); + }); + + testUsingContext('runs CocoaPods Pod runner script migrator', () async { + final FlutterProject projectUnderTest = setupProjectUnderTest(); + pretendPodIsInstalled(); + pretendPodVersionIs('100.0.0'); + projectUnderTest.ios.podfile + ..createSync() + ..writeAsStringSync('Existing Podfile'); + projectUnderTest.ios.podfileLock + ..createSync() + ..writeAsStringSync('Existing lock file.'); + projectUnderTest.ios.podManifestLock + ..createSync(recursive: true) + ..writeAsStringSync('Existing lock file.'); + projectUnderTest.ios.podRunnerFrameworksScript + ..createSync(recursive: true) + ..writeAsStringSync(r'source="$(readlink "${source}")"'); + + fakeProcessManager.addCommands(const [ + FakeCommand( + command: ['pod', 'install', '--verbose'], + workingDirectory: 'project/ios', + environment: {'COCOAPODS_DISABLE_STATS': 'true', 'LANG': 'en_US.UTF-8'}, + ), + FakeCommand( + command: ['touch', 'project/ios/Podfile.lock'], + ), + ]); + + final CocoaPods cocoaPodsUnderTestXcode143 = CocoaPods( + fileSystem: fileSystem, + processManager: fakeProcessManager, + logger: logger, + platform: FakePlatform(operatingSystem: 'macos'), + xcodeProjectInterpreter: XcodeProjectInterpreter.test(processManager: fakeProcessManager, version: Version(14, 3, 0)), + usage: usage, + ); + + final bool didInstall = await cocoaPodsUnderTestXcode143.processPods( + xcodeProject: projectUnderTest.ios, + buildMode: BuildMode.debug, + ); + expect(didInstall, isTrue); + expect(fakeProcessManager, hasNoRemainingExpectations); + // Now has readlink -f flag. + expect(projectUnderTest.ios.podRunnerFrameworksScript.readAsStringSync(), contains(r'source="$(readlink -f "${source}")"')); + expect(logger.statusText, contains('Upgrading Pods-Runner-frameworks.sh')); }); testUsingContext('runs pod install, if Podfile.lock is older than Podfile', () async {