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
diff --git a/packages/flutter_tools/lib/src/macos/cocoapods.dart b/packages/flutter_tools/lib/src/macos/cocoapods.dart
index a131de0..07cad54 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/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 @@
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(<ProjectMigrator>[
+ 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 0000000..f0d90ca
--- /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 ca08f89..485be40 100644
--- a/packages/flutter_tools/lib/src/xcode_project.dart
+++ b/packages/flutter_tools/lib/src/xcode_project.dart
@@ -89,6 +89,13 @@
/// 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 6326e43..beb40cb 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/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/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:test/fake.dart';
import '../../src/common.dart';
+import '../../src/fake_process_manager.dart';
void main () {
group('iOS migration', () {
@@ -900,6 +904,104 @@
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 @@
@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 3c34f14..a03134e 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/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 @@
);
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>[
+ FakeCommand(
+ command: <String>['pod', 'install', '--verbose'],
+ workingDirectory: 'project/ios',
+ environment: <String, String>{'COCOAPODS_DISABLE_STATS': 'true', 'LANG': 'en_US.UTF-8'},
+ ),
+ FakeCommand(
+ command: <String>['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 {