Skip to content

Commit 4b12645

Browse files
authored
Add readlink -f flag to CocoaPods script to workaround Xcode 14.3 issue (flutter#124079)
Cherry-pick flutter#124062 onto stable. CP request at flutter#124081
1 parent 62bd795 commit 4b12645

File tree

5 files changed

+223
-0
lines changed

5 files changed

+223
-0
lines changed

packages/flutter_tools/lib/src/macos/cocoapods.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@ import '../base/logger.dart';
1313
import '../base/os.dart';
1414
import '../base/platform.dart';
1515
import '../base/process.dart';
16+
import '../base/project_migrator.dart';
1617
import '../base/version.dart';
1718
import '../build_info.dart';
1819
import '../cache.dart';
1920
import '../ios/xcodeproj.dart';
21+
import '../migrations/cocoapods_script_symlink.dart';
2022
import '../reporting/reporting.dart';
2123
import '../xcode_project.dart';
2224

@@ -166,6 +168,13 @@ class CocoaPods {
166168
throwToolExit('CocoaPods not installed or not in valid state.');
167169
}
168170
await _runPodInstall(xcodeProject, buildMode);
171+
172+
// This migrator works around a CocoaPods bug, and should be run after `pod install` is run.
173+
final ProjectMigration postPodMigration = ProjectMigration(<ProjectMigrator>[
174+
CocoaPodsScriptReadlink(xcodeProject, _xcodeProjectInterpreter, _logger),
175+
]);
176+
postPodMigration.run();
177+
169178
podsProcessed = true;
170179
}
171180
return podsProcessed;
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import '../base/file_system.dart';
6+
import '../base/project_migrator.dart';
7+
import '../base/version.dart';
8+
import '../ios/xcodeproj.dart';
9+
import '../xcode_project.dart';
10+
11+
// Xcode 14.3 changed the readlink symlink behavior to be relative from the script working directory, instead of the
12+
// relative path of the symlink. The -f flag returns the original "--canonicalize" behavior the CocoaPods script relies on.
13+
// This has been fixed upstream in CocoaPods, but migrate a copy of their workaround so users don't need to update.
14+
//
15+
// See https://github.com/flutter/flutter/issues/123890#issuecomment-1494825976.
16+
class CocoaPodsScriptReadlink extends ProjectMigrator {
17+
CocoaPodsScriptReadlink(
18+
XcodeBasedProject project,
19+
XcodeProjectInterpreter xcodeProjectInterpreter,
20+
super.logger,
21+
) : _podRunnerFrameworksScript = project.podRunnerFrameworksScript,
22+
_xcodeProjectInterpreter = xcodeProjectInterpreter;
23+
24+
final File _podRunnerFrameworksScript;
25+
final XcodeProjectInterpreter _xcodeProjectInterpreter;
26+
27+
@override
28+
void migrate() {
29+
if (!_podRunnerFrameworksScript.existsSync()) {
30+
logger.printTrace('CocoaPods Pods-Runner-frameworks.sh script not found, skipping "readlink -f" workaround.');
31+
return;
32+
}
33+
34+
final Version? version = _xcodeProjectInterpreter.version;
35+
36+
// If Xcode not installed or less than 14.3 with readlink behavior change, skip this migration.
37+
if (version == null || version < Version(14, 3, 0)) {
38+
logger.printTrace('Detected Xcode version is $version, below 14.3, skipping "readlink -f" workaround.');
39+
return;
40+
}
41+
42+
processFileLines(_podRunnerFrameworksScript);
43+
}
44+
45+
@override
46+
String? migrateLine(String line) {
47+
const String originalReadLinkLine = r'source="$(readlink "${source}")"';
48+
const String replacementReadLinkLine = r'source="$(readlink -f "${source}")"';
49+
50+
return line.replaceAll(originalReadLinkLine, replacementReadLinkLine);
51+
}
52+
}

packages/flutter_tools/lib/src/xcode_project.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,13 @@ abstract class XcodeBasedProject extends FlutterProjectPlatform {
8989

9090
/// The CocoaPods 'Manifest.lock'.
9191
File get podManifestLock => hostAppRoot.childDirectory('Pods').childFile('Manifest.lock');
92+
93+
/// The CocoaPods generated 'Pods-Runner-frameworks.sh'.
94+
File get podRunnerFrameworksScript => hostAppRoot
95+
.childDirectory('Pods')
96+
.childDirectory('Target Support Files')
97+
.childDirectory('Pods-Runner')
98+
.childFile('Pods-Runner-frameworks.sh');
9299
}
93100

94101
/// Represents the iOS sub-project of a Flutter project.

packages/flutter_tools/test/general.shard/ios/ios_project_migration_test.dart

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,24 @@ import 'package:file/file.dart';
66
import 'package:file/memory.dart';
77
import 'package:flutter_tools/src/base/logger.dart';
88
import 'package:flutter_tools/src/base/project_migrator.dart';
9+
import 'package:flutter_tools/src/base/version.dart';
910
import 'package:flutter_tools/src/ios/migrations/host_app_info_plist_migration.dart';
1011
import 'package:flutter_tools/src/ios/migrations/ios_deployment_target_migration.dart';
1112
import 'package:flutter_tools/src/ios/migrations/project_base_configuration_migration.dart';
1213
import 'package:flutter_tools/src/ios/migrations/project_build_location_migration.dart';
1314
import 'package:flutter_tools/src/ios/migrations/remove_bitcode_migration.dart';
1415
import 'package:flutter_tools/src/ios/migrations/remove_framework_link_and_embedding_migration.dart';
1516
import 'package:flutter_tools/src/ios/migrations/xcode_build_system_migration.dart';
17+
import 'package:flutter_tools/src/ios/xcodeproj.dart';
18+
import 'package:flutter_tools/src/migrations/cocoapods_script_symlink.dart';
1619
import 'package:flutter_tools/src/migrations/xcode_project_object_version_migration.dart';
1720
import 'package:flutter_tools/src/migrations/xcode_script_build_phase_migration.dart';
1821
import 'package:flutter_tools/src/reporting/reporting.dart';
1922
import 'package:flutter_tools/src/xcode_project.dart';
2023
import 'package:test/fake.dart';
2124

2225
import '../../src/common.dart';
26+
import '../../src/fake_process_manager.dart';
2327

2428
void main () {
2529
group('iOS migration', () {
@@ -900,6 +904,104 @@ platform :ios, '11.0'
900904
expect('Disabling deprecated bitcode Xcode build setting'.allMatches(testLogger.warningText).length, 1);
901905
});
902906
});
907+
908+
group('CocoaPods script readlink', () {
909+
late MemoryFileSystem memoryFileSystem;
910+
late BufferLogger testLogger;
911+
late FakeIosProject project;
912+
late File podRunnerFrameworksScript;
913+
late ProcessManager processManager;
914+
late XcodeProjectInterpreter xcode143ProjectInterpreter;
915+
916+
setUp(() {
917+
memoryFileSystem = MemoryFileSystem();
918+
podRunnerFrameworksScript = memoryFileSystem.file('Pods-Runner-frameworks.sh');
919+
testLogger = BufferLogger.test();
920+
project = FakeIosProject();
921+
processManager = FakeProcessManager.any();
922+
xcode143ProjectInterpreter = XcodeProjectInterpreter.test(processManager: processManager, version: Version(14, 3, 0));
923+
project.podRunnerFrameworksScript = podRunnerFrameworksScript;
924+
});
925+
926+
testWithoutContext('skipped if files are missing', () {
927+
final CocoaPodsScriptReadlink iosProjectMigration = CocoaPodsScriptReadlink(
928+
project,
929+
xcode143ProjectInterpreter,
930+
testLogger,
931+
);
932+
iosProjectMigration.migrate();
933+
expect(podRunnerFrameworksScript.existsSync(), isFalse);
934+
935+
expect(testLogger.traceText, contains('CocoaPods Pods-Runner-frameworks.sh script not found'));
936+
expect(testLogger.statusText, isEmpty);
937+
});
938+
939+
testWithoutContext('skipped if nothing to upgrade', () {
940+
const String contents = r'''
941+
if [ -L "${source}" ]; then
942+
echo "Symlinked..."
943+
source="$(readlink -f "${source}")"
944+
fi''';
945+
podRunnerFrameworksScript.writeAsStringSync(contents);
946+
947+
final CocoaPodsScriptReadlink iosProjectMigration = CocoaPodsScriptReadlink(
948+
project,
949+
xcode143ProjectInterpreter,
950+
testLogger,
951+
);
952+
iosProjectMigration.migrate();
953+
expect(podRunnerFrameworksScript.existsSync(), isTrue);
954+
expect(testLogger.traceText, isEmpty);
955+
expect(testLogger.statusText, isEmpty);
956+
});
957+
958+
testWithoutContext('skipped if Xcode version below 14.3', () {
959+
const String contents = r'''
960+
if [ -L "${source}" ]; then
961+
echo "Symlinked..."
962+
source="$(readlink "${source}")"
963+
fi''';
964+
podRunnerFrameworksScript.writeAsStringSync(contents);
965+
966+
final XcodeProjectInterpreter xcode142ProjectInterpreter = XcodeProjectInterpreter.test(
967+
processManager: processManager,
968+
version: Version(14, 2, 0),
969+
);
970+
971+
final CocoaPodsScriptReadlink iosProjectMigration = CocoaPodsScriptReadlink(
972+
project,
973+
xcode142ProjectInterpreter,
974+
testLogger,
975+
);
976+
iosProjectMigration.migrate();
977+
expect(podRunnerFrameworksScript.existsSync(), isTrue);
978+
expect(testLogger.traceText, contains('Detected Xcode version is 14.2.0, below 14.3, skipping "readlink -f" workaround'));
979+
expect(testLogger.statusText, isEmpty);
980+
});
981+
982+
testWithoutContext('Xcode project is migrated', () {
983+
const String contents = r'''
984+
if [ -L "${source}" ]; then
985+
echo "Symlinked..."
986+
source="$(readlink "${source}")"
987+
fi''';
988+
podRunnerFrameworksScript.writeAsStringSync(contents);
989+
990+
final CocoaPodsScriptReadlink iosProjectMigration = CocoaPodsScriptReadlink(
991+
project,
992+
xcode143ProjectInterpreter,
993+
testLogger,
994+
);
995+
iosProjectMigration.migrate();
996+
expect(podRunnerFrameworksScript.readAsStringSync(), r'''
997+
if [ -L "${source}" ]; then
998+
echo "Symlinked..."
999+
source="$(readlink -f "${source}")"
1000+
fi
1001+
''');
1002+
expect(testLogger.statusText, contains('Upgrading Pods-Runner-frameworks.sh'));
1003+
});
1004+
});
9031005
});
9041006

9051007
group('update Xcode script build phase', () {
@@ -1025,6 +1127,9 @@ class FakeIosProject extends Fake implements IosProject {
10251127

10261128
@override
10271129
File podfile = MemoryFileSystem.test().file('Podfile');
1130+
1131+
@override
1132+
File podRunnerFrameworksScript = MemoryFileSystem.test().file('podRunnerFrameworksScript');
10281133
}
10291134

10301135
class FakeIOSMigrator extends ProjectMigrator {

packages/flutter_tools/test/general.shard/macos/cocoapods_test.dart

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'package:file/file.dart';
66
import 'package:file/memory.dart';
77
import 'package:flutter_tools/src/base/logger.dart';
88
import 'package:flutter_tools/src/base/platform.dart';
9+
import 'package:flutter_tools/src/base/version.dart';
910
import 'package:flutter_tools/src/build_info.dart';
1011
import 'package:flutter_tools/src/cache.dart';
1112
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
729730
);
730731
expect(didInstall, isTrue);
731732
expect(fakeProcessManager, hasNoRemainingExpectations);
733+
expect(logger.traceText, contains('CocoaPods Pods-Runner-frameworks.sh script not found'));
734+
});
735+
736+
testUsingContext('runs CocoaPods Pod runner script migrator', () async {
737+
final FlutterProject projectUnderTest = setupProjectUnderTest();
738+
pretendPodIsInstalled();
739+
pretendPodVersionIs('100.0.0');
740+
projectUnderTest.ios.podfile
741+
..createSync()
742+
..writeAsStringSync('Existing Podfile');
743+
projectUnderTest.ios.podfileLock
744+
..createSync()
745+
..writeAsStringSync('Existing lock file.');
746+
projectUnderTest.ios.podManifestLock
747+
..createSync(recursive: true)
748+
..writeAsStringSync('Existing lock file.');
749+
projectUnderTest.ios.podRunnerFrameworksScript
750+
..createSync(recursive: true)
751+
..writeAsStringSync(r'source="$(readlink "${source}")"');
752+
753+
fakeProcessManager.addCommands(const <FakeCommand>[
754+
FakeCommand(
755+
command: <String>['pod', 'install', '--verbose'],
756+
workingDirectory: 'project/ios',
757+
environment: <String, String>{'COCOAPODS_DISABLE_STATS': 'true', 'LANG': 'en_US.UTF-8'},
758+
),
759+
FakeCommand(
760+
command: <String>['touch', 'project/ios/Podfile.lock'],
761+
),
762+
]);
763+
764+
final CocoaPods cocoaPodsUnderTestXcode143 = CocoaPods(
765+
fileSystem: fileSystem,
766+
processManager: fakeProcessManager,
767+
logger: logger,
768+
platform: FakePlatform(operatingSystem: 'macos'),
769+
xcodeProjectInterpreter: XcodeProjectInterpreter.test(processManager: fakeProcessManager, version: Version(14, 3, 0)),
770+
usage: usage,
771+
);
772+
773+
final bool didInstall = await cocoaPodsUnderTestXcode143.processPods(
774+
xcodeProject: projectUnderTest.ios,
775+
buildMode: BuildMode.debug,
776+
);
777+
expect(didInstall, isTrue);
778+
expect(fakeProcessManager, hasNoRemainingExpectations);
779+
// Now has readlink -f flag.
780+
expect(projectUnderTest.ios.podRunnerFrameworksScript.readAsStringSync(), contains(r'source="$(readlink -f "${source}")"'));
781+
expect(logger.statusText, contains('Upgrading Pods-Runner-frameworks.sh'));
732782
});
733783

734784
testUsingContext('runs pod install, if Podfile.lock is older than Podfile', () async {

0 commit comments

Comments
 (0)