Unverified Commit d628a6ff authored by stuartmorgan's avatar stuartmorgan Committed by GitHub

Give an actionable error message when a Pod requires a higher minimum OS version (#138097)

Checks `pod install` output for the case where a pod requires a higher minimum OS deployment target version than the app is set to use, and attempts to turn it into a more actionable error message. This isn't foolproof since we are parsing the Ruby rather than actually executing it, but I would expect that the vast majority of cases would end up in the most useful version (and even those that don't are still much clearer with this as the final error message text than without it).

Fixes https://github.com/flutter/flutter/issues/113762
parent 311193d3
......@@ -355,7 +355,7 @@ class CocoaPods {
if (result.exitCode != 0) {
invalidatePodInstallOutput(xcodeProject);
_diagnosePodInstallFailure(result);
_diagnosePodInstallFailure(result, xcodeProject);
throwToolExit('Error running pod install');
} else if (xcodeProject.podfileLock.existsSync()) {
// Even if the Podfile.lock didn't change, update its modified date to now
......@@ -367,7 +367,7 @@ class CocoaPods {
}
}
void _diagnosePodInstallFailure(ProcessResult result) {
void _diagnosePodInstallFailure(ProcessResult result, XcodeBasedProject xcodeProject) {
final Object? stdout = result.stdout;
final Object? stderr = result.stderr;
if (stdout is! String || stderr is! String) {
......@@ -397,7 +397,130 @@ class CocoaPods {
' sudo gem uninstall ffi && sudo gem install ffi -- --enable-libffi-alloc\n',
emphasis: true,
);
} else if (stdout.contains('required a higher minimum deployment target')) {
final ({String failingPod, String sourcePlugin, String podPluginSubdir})?
podInfo = _parseMinDeploymentFailureInfo(stdout);
if (podInfo != null) {
final String sourcePlugin = podInfo.sourcePlugin;
// If the plugin's podfile has set its own minimum version correctly
// based on the requirements of its dependencies the failing pod should
// be the plugin itself, but if not they may be different (e.g., if
// a plugin says its minimum iOS version is 11, but depends on a pod
// with a minimum version of 12, then building for 11 will report that
// pod as failing.)
if (podInfo.failingPod == podInfo.sourcePlugin) {
final Directory symlinksDir;
final String podPlatformString;
final String platformName;
final String docsLink;
if (xcodeProject is IosProject) {
symlinksDir = xcodeProject.symlinks;
podPlatformString = 'ios';
platformName = 'iOS';
docsLink = 'https://docs.flutter.dev/deployment/ios';
} else if (xcodeProject is MacOSProject) {
symlinksDir = xcodeProject.ephemeralDirectory.childDirectory('.symlinks');
podPlatformString = 'osx';
platformName = 'macOS';
docsLink = 'https://docs.flutter.dev/deployment/macos';
} else {
return;
}
final File podspec = symlinksDir
.childDirectory('plugins')
.childDirectory(sourcePlugin)
.childDirectory(podInfo.podPluginSubdir)
.childFile('$sourcePlugin.podspec');
final String? minDeploymentVersion = _findPodspecMinDeploymentVersion(
podspec,
podPlatformString
);
if (minDeploymentVersion != null) {
_logger.printError(
'Error: The plugin "$sourcePlugin" requires a higher minimum '
'$platformName deployment version than your application is targeting.\n'
"To build, increase your application's deployment target to at "
'least $minDeploymentVersion as described at $docsLink',
emphasis: true,
);
} else {
// If for some reason the min version can't be parsed out, provide
// a less specific error message that still describes the problem,
// but also requests filing a Flutter issue so the parsing in
// _findPodspecMinDeploymentVersion can be improved.
_logger.printError(
'Error: The plugin "$sourcePlugin" requires a higher minimum '
'$platformName deployment version than your application is targeting.\n'
"To build, increase your application's deployment target as "
'described at $docsLink\n\n'
'The minimum required version for "$sourcePlugin" could not be '
'determined. Please file an issue at '
'https://github.com/flutter/flutter/issues about this error message.',
emphasis: true,
);
}
} else {
// In theory this could find the failing pod's spec and parse out its
// minimum deployment version, but finding that spec would add a lot
// of complexity to handle a case that plugin authors should not
// create, so this just provides the actionable step of following up
// with the plugin developer.
_logger.printError(
'Error: The pod "${podInfo.failingPod}" required by the plugin '
'"$sourcePlugin" requires a higher minimum iOS deployment version '
"than the plugin's reported minimum version.\n"
'To build, remove the plugin "$sourcePlugin", or contact the plugin\'s '
'developers for assistance.',
emphasis: true,
);
}
}
}
}
({String failingPod, String sourcePlugin, String podPluginSubdir})?
_parseMinDeploymentFailureInfo(String podInstallOutput) {
final RegExp sourceLine = RegExp(r'\(from `.*\.symlinks/plugins/([^/]+)/([^/]+)`\)');
final RegExp dependencyLine = RegExp(r'Specs satisfying the `([^ ]+).*` dependency were found, '
'but they required a higher minimum deployment target');
final RegExpMatch? sourceMatch = sourceLine.firstMatch(podInstallOutput);
final RegExpMatch? dependencyMatch = dependencyLine.firstMatch(podInstallOutput);
if (sourceMatch == null || dependencyMatch == null) {
return null;
}
return (
failingPod: dependencyMatch.group(1)!,
sourcePlugin: sourceMatch.group(1)!,
podPluginSubdir: sourceMatch.group(2)!
);
}
String? _findPodspecMinDeploymentVersion(File podspec, String platformString) {
if (!podspec.existsSync()) {
return null;
}
// There are two ways the deployment target can be specified; see
// https://guides.cocoapods.org/syntax/podspec.html#group_platform
final RegExp platformPattern = RegExp(
// Example: spec.platform = :osx, '10.8'
// where "spec" is an arbitrary variable name.
r'^\s*[a-zA-Z_]+\.platform\s*=\s*'
':$platformString'
r'''\s*,\s*["']([^"']+)["']''',
multiLine: true
);
final RegExp deploymentTargetPlatform = RegExp(
// Example: spec.osx.deployment_target = '10.8'
// where "spec" is an arbitrary variable name.
r'^\s*[a-zA-Z_]+\.'
'$platformString\\.deployment_target'
r'''\s*=\s*["']([^"']+)["']''',
multiLine: true
);
final String podspecContents = podspec.readAsStringSync();
final RegExpMatch? match = platformPattern.firstMatch(podspecContents) ??
deploymentTargetPlatform.firstMatch(podspecContents);
return match?.group(1);
}
bool _isFfiX86Error(String error) {
......
......@@ -502,6 +502,448 @@ Note: as of CocoaPods 1.0, `pod repo update` does not happen on `pod install` by
);
});
testUsingContext('throws if plugin requires higher minimum iOS version using "platform"', () async {
final FlutterProject projectUnderTest = setupProjectUnderTest();
pretendPodIsInstalled();
pretendPodVersionIs('100.0.0');
fileSystem.file(fileSystem.path.join('project', 'ios', 'Podfile'))
..createSync()
..writeAsStringSync('Existing Podfile');
const String fakePluginName = 'some_plugin';
final File podspec = projectUnderTest.ios.symlinks
.childDirectory('plugins')
.childDirectory(fakePluginName)
.childDirectory('ios')
.childFile('$fakePluginName.podspec');
podspec.createSync(recursive: true);
podspec.writeAsStringSync('''
Pod::Spec.new do |s|
s.name = '$fakePluginName'
s.version = '0.0.1'
s.summary = 'A plugin'
s.source_files = 'Classes/**/*.{h,m}'
s.dependency 'Flutter'
s.static_framework = true
s.platform = :ios, '15.0'
end''');
fakeProcessManager.addCommand(
FakeCommand(
command: const <String>['pod', 'install', '--verbose'],
workingDirectory: 'project/ios',
environment: const <String, String>{
'COCOAPODS_DISABLE_STATS': 'true',
'LANG': 'en_US.UTF-8',
},
exitCode: 1,
stdout: _fakeHigherMinimumIOSVersionPodInstallOutput(fakePluginName),
),
);
await expectLater(cocoaPodsUnderTest.processPods(
xcodeProject: projectUnderTest.ios,
buildMode: BuildMode.debug,
), throwsToolExit());
expect(
logger.errorText,
contains(
'The plugin "$fakePluginName" requires a higher minimum iOS '
'deployment version than your application is targeting.'
),
);
// The error should contain specific instructions for fixing the build
// based on parsing the plugin's podspec.
expect(
logger.errorText,
contains(
"To build, increase your application's deployment target to at least "
'15.0 as described at https://docs.flutter.dev/deployment/ios'
),
);
});
testUsingContext('throws if plugin requires higher minimum iOS version using "deployment_target"', () async {
final FlutterProject projectUnderTest = setupProjectUnderTest();
pretendPodIsInstalled();
pretendPodVersionIs('100.0.0');
fileSystem.file(fileSystem.path.join('project', 'ios', 'Podfile'))
..createSync()
..writeAsStringSync('Existing Podfile');
const String fakePluginName = 'some_plugin';
final File podspec = projectUnderTest.ios.symlinks
.childDirectory('plugins')
.childDirectory(fakePluginName)
.childDirectory('ios')
.childFile('$fakePluginName.podspec');
podspec.createSync(recursive: true);
podspec.writeAsStringSync('''
Pod::Spec.new do |s|
s.name = '$fakePluginName'
s.version = '0.0.1'
s.summary = 'A plugin'
s.source_files = 'Classes/**/*.{h,m}'
s.dependency 'Flutter'
s.static_framework = true
s.ios.deployment_target = '15.0'
end''');
fakeProcessManager.addCommand(
FakeCommand(
command: const <String>['pod', 'install', '--verbose'],
workingDirectory: 'project/ios',
environment: const <String, String>{
'COCOAPODS_DISABLE_STATS': 'true',
'LANG': 'en_US.UTF-8',
},
exitCode: 1,
stdout: _fakeHigherMinimumIOSVersionPodInstallOutput(fakePluginName),
),
);
await expectLater(cocoaPodsUnderTest.processPods(
xcodeProject: projectUnderTest.ios,
buildMode: BuildMode.debug,
), throwsToolExit());
expect(
logger.errorText,
contains(
'The plugin "$fakePluginName" requires a higher minimum iOS '
'deployment version than your application is targeting.'
),
);
// The error should contain specific instructions for fixing the build
// based on parsing the plugin's podspec.
expect(
logger.errorText,
contains(
"To build, increase your application's deployment target to at least "
'15.0 as described at https://docs.flutter.dev/deployment/ios'
),
);
});
testUsingContext('throws if plugin requires higher minimum iOS version with darwin layout', () async {
final FlutterProject projectUnderTest = setupProjectUnderTest();
pretendPodIsInstalled();
pretendPodVersionIs('100.0.0');
fileSystem.file(fileSystem.path.join('project', 'ios', 'Podfile'))
..createSync()
..writeAsStringSync('Existing Podfile');
const String fakePluginName = 'some_plugin';
final File podspec = projectUnderTest.ios.symlinks
.childDirectory('plugins')
.childDirectory(fakePluginName)
.childDirectory('darwin')
.childFile('$fakePluginName.podspec');
podspec.createSync(recursive: true);
podspec.writeAsStringSync('''
Pod::Spec.new do |s|
s.name = '$fakePluginName'
s.version = '0.0.1'
s.summary = 'A plugin'
s.source_files = 'Classes/**/*.{h,m}'
s.dependency 'Flutter'
s.static_framework = true
s.osx.deployment_target = '10.15'
s.ios.deployment_target = '15.0'
end''');
fakeProcessManager.addCommand(
FakeCommand(
command: const <String>['pod', 'install', '--verbose'],
workingDirectory: 'project/ios',
environment: const <String, String>{
'COCOAPODS_DISABLE_STATS': 'true',
'LANG': 'en_US.UTF-8',
},
exitCode: 1,
stdout: _fakeHigherMinimumIOSVersionPodInstallOutput(fakePluginName, subdir: 'darwin'),
),
);
await expectLater(cocoaPodsUnderTest.processPods(
xcodeProject: projectUnderTest.ios,
buildMode: BuildMode.debug,
), throwsToolExit());
expect(
logger.errorText,
contains(
'The plugin "$fakePluginName" requires a higher minimum iOS '
'deployment version than your application is targeting.'
),
);
// The error should contain specific instructions for fixing the build
// based on parsing the plugin's podspec.
expect(
logger.errorText,
contains(
"To build, increase your application's deployment target to at least "
'15.0 as described at https://docs.flutter.dev/deployment/ios'
),
);
});
testUsingContext('throws if plugin requires unknown higher minimum iOS version', () async {
final FlutterProject projectUnderTest = setupProjectUnderTest();
pretendPodIsInstalled();
pretendPodVersionIs('100.0.0');
fileSystem.file(fileSystem.path.join('project', 'ios', 'Podfile'))
..createSync()
..writeAsStringSync('Existing Podfile');
const String fakePluginName = 'some_plugin';
final File podspec = projectUnderTest.ios.symlinks
.childDirectory('plugins')
.childDirectory(fakePluginName)
.childDirectory('ios')
.childFile('$fakePluginName.podspec');
podspec.createSync(recursive: true);
// It's very unlikely that someone would actually ever do anything like
// this, but arbitrary code is possible, so test that if it's not what
// the error handler parsing expects, a fallback is used.
podspec.writeAsStringSync('''
Pod::Spec.new do |s|
s.name = '$fakePluginName'
s.version = '0.0.1'
s.summary = 'A plugin'
s.source_files = 'Classes/**/*.{h,m}'
s.dependency 'Flutter'
s.static_framework = true
version_var = '15.0'
s.platform = :ios, version_var
end''');
fakeProcessManager.addCommand(
FakeCommand(
command: const <String>['pod', 'install', '--verbose'],
workingDirectory: 'project/ios',
environment: const <String, String>{
'COCOAPODS_DISABLE_STATS': 'true',
'LANG': 'en_US.UTF-8',
},
exitCode: 1,
stdout: _fakeHigherMinimumIOSVersionPodInstallOutput(fakePluginName),
),
);
await expectLater(cocoaPodsUnderTest.processPods(
xcodeProject: projectUnderTest.ios,
buildMode: BuildMode.debug,
), throwsToolExit());
expect(
logger.errorText,
contains(
'The plugin "$fakePluginName" requires a higher minimum iOS '
'deployment version than your application is targeting.'
),
);
// The error should contain non-specific instructions for fixing the build
// and note that the minimum version could not be determined.
expect(
logger.errorText,
contains(
"To build, increase your application's deployment target as "
'described at https://docs.flutter.dev/deployment/ios',
),
);
expect(
logger.errorText,
contains(
'The minimum required version for "$fakePluginName" could not be '
'determined',
),
);
});
testUsingContext('throws if plugin has a dependency that requires a higher minimum iOS version', () async {
final FlutterProject projectUnderTest = setupProjectUnderTest();
pretendPodIsInstalled();
pretendPodVersionIs('100.0.0');
fileSystem.file(fileSystem.path.join('project', 'ios', 'Podfile'))
..createSync()
..writeAsStringSync('Existing Podfile');
fakeProcessManager.addCommand(
const FakeCommand(
command: <String>['pod', 'install', '--verbose'],
workingDirectory: 'project/ios',
environment: <String, String>{
'COCOAPODS_DISABLE_STATS': 'true',
'LANG': 'en_US.UTF-8',
},
exitCode: 1,
// This is the (very slightly abridged) output from updating the
// minimum version of the GoogleMaps dependency in
// google_maps_flutter_ios without updating the minimum iOS version to
// match, as an example of a misconfigured plugin.
stdout: '''
Analyzing dependencies
Inspecting targets to integrate
Using `ARCHS` setting to build architectures of target `Pods-Runner`: (``)
Using `ARCHS` setting to build architectures of target `Pods-RunnerTests`: (``)
Fetching external sources
-> Fetching podspec for `Flutter` from `Flutter`
-> Fetching podspec for `google_maps_flutter_ios` from `.symlinks/plugins/google_maps_flutter_ios/ios`
Resolving dependencies of `Podfile`
CDN: trunk Relative path: CocoaPods-version.yml exists! Returning local because checking is only performed in repo update
CDN: trunk Relative path: Specs/a/d/d/GoogleMaps/8.0.0/GoogleMaps.podspec.json exists! Returning local because checking is only performed in repo update
[!] CocoaPods could not find compatible versions for pod "GoogleMaps":
In Podfile:
google_maps_flutter_ios (from `.symlinks/plugins/google_maps_flutter_ios/ios`) was resolved to 0.0.1, which depends on
GoogleMaps (~> 8.0)
Specs satisfying the `GoogleMaps (~> 8.0)` dependency were found, but they required a higher minimum deployment target.''',
),
);
await expectLater(cocoaPodsUnderTest.processPods(
xcodeProject: projectUnderTest.ios,
buildMode: BuildMode.debug,
), throwsToolExit());
expect(
logger.errorText,
contains(
'The pod "GoogleMaps" required by the plugin "google_maps_flutter_ios" '
"requires a higher minimum iOS deployment version than the plugin's "
'reported minimum version.'
),
);
// The error should tell the user to contact the plugin author, as this
// case is hard for us to give exact advice on, and should only be
// possible if there's a mistake in the plugin's podspec.
expect(
logger.errorText,
contains(
'To build, remove the plugin "google_maps_flutter_ios", or contact '
"the plugin's developers for assistance.",
),
);
});
testUsingContext('throws if plugin requires higher minimum macOS version using "platform"', () async {
final FlutterProject projectUnderTest = setupProjectUnderTest();
pretendPodIsInstalled();
pretendPodVersionIs('100.0.0');
fileSystem.file(fileSystem.path.join('project', 'macos', 'Podfile'))
..createSync()
..writeAsStringSync('Existing Podfile');
const String fakePluginName = 'some_plugin';
final File podspec = projectUnderTest.macos.ephemeralDirectory
.childDirectory('.symlinks')
.childDirectory('plugins')
.childDirectory(fakePluginName)
.childDirectory('macos')
.childFile('$fakePluginName.podspec');
podspec.createSync(recursive: true);
podspec.writeAsStringSync('''
Pod::Spec.new do |spec|
spec.name = '$fakePluginName'
spec.version = '0.0.1'
spec.summary = 'A plugin'
spec.source_files = 'Classes/**/*.swift'
spec.dependency 'FlutterMacOS'
spec.static_framework = true
spec.platform = :osx, "12.7"
end''');
fakeProcessManager.addCommand(
FakeCommand(
command: const <String>['pod', 'install', '--verbose'],
workingDirectory: 'project/macos',
environment: const <String, String>{
'COCOAPODS_DISABLE_STATS': 'true',
'LANG': 'en_US.UTF-8',
},
exitCode: 1,
stdout: _fakeHigherMinimumMacOSVersionPodInstallOutput(fakePluginName),
),
);
await expectLater(cocoaPodsUnderTest.processPods(
xcodeProject: projectUnderTest.macos,
buildMode: BuildMode.debug,
), throwsToolExit());
expect(
logger.errorText,
contains(
'The plugin "$fakePluginName" requires a higher minimum macOS '
'deployment version than your application is targeting.'
),
);
// The error should contain specific instructions for fixing the build
// based on parsing the plugin's podspec.
expect(
logger.errorText,
contains(
"To build, increase your application's deployment target to at least "
'12.7 as described at https://docs.flutter.dev/deployment/macos'
),
);
});
testUsingContext('throws if plugin requires higher minimum macOS version using "deployment_target"', () async {
final FlutterProject projectUnderTest = setupProjectUnderTest();
pretendPodIsInstalled();
pretendPodVersionIs('100.0.0');
fileSystem.file(fileSystem.path.join('project', 'macos', 'Podfile'))
..createSync()
..writeAsStringSync('Existing Podfile');
const String fakePluginName = 'some_plugin';
final File podspec = projectUnderTest.macos.ephemeralDirectory
.childDirectory('.symlinks')
.childDirectory('plugins')
.childDirectory(fakePluginName)
.childDirectory('macos')
.childFile('$fakePluginName.podspec');
podspec.createSync(recursive: true);
podspec.writeAsStringSync('''
Pod::Spec.new do |spec|
spec.name = '$fakePluginName'
spec.version = '0.0.1'
spec.summary = 'A plugin'
spec.source_files = 'Classes/**/*.{h,m}'
spec.dependency 'Flutter'
spec.static_framework = true
spec.osx.deployment_target = '12.7'
end''');
fakeProcessManager.addCommand(
FakeCommand(
command: const <String>['pod', 'install', '--verbose'],
workingDirectory: 'project/macos',
environment: const <String, String>{
'COCOAPODS_DISABLE_STATS': 'true',
'LANG': 'en_US.UTF-8',
},
exitCode: 1,
stdout: _fakeHigherMinimumMacOSVersionPodInstallOutput(fakePluginName),
),
);
await expectLater(cocoaPodsUnderTest.processPods(
xcodeProject: projectUnderTest.macos,
buildMode: BuildMode.debug,
), throwsToolExit());
expect(
logger.errorText,
contains(
'The plugin "$fakePluginName" requires a higher minimum macOS '
'deployment version than your application is targeting.'
),
);
// The error should contain specific instructions for fixing the build
// based on parsing the plugin's podspec.
expect(
logger.errorText,
contains(
"To build, increase your application's deployment target to at least "
'12.7 as described at https://docs.flutter.dev/deployment/macos'
),
);
});
final Map<String, String> possibleErrors = <String, String>{
'symbol not found': 'LoadError - dlsym(0x7fbbeb6837d0, Init_ffi_c): symbol not found - /Library/Ruby/Gems/2.6.0/gems/ffi-1.13.1/lib/ffi_c.bundle',
'incompatible architecture': "LoadError - (mach-o file, but is an incompatible architecture (have 'arm64', need 'x86_64')), '/usr/lib/ffi_c.bundle' (no such file) - /Library/Ruby/Gems/2.6.0/gems/ffi-1.15.4/lib/ffi_c.bundle",
......@@ -889,6 +1331,54 @@ Note: as of CocoaPods 1.0, `pod repo update` does not happen on `pod install` by
});
}
String _fakeHigherMinimumIOSVersionPodInstallOutput(String fakePluginName, {String subdir = 'ios'}) {
return '''
Preparing
Analyzing dependencies
Inspecting targets to integrate
Using `ARCHS` setting to build architectures of target `Pods-Runner`: (``)
Using `ARCHS` setting to build architectures of target `Pods-RunnerTests`: (``)
Fetching external sources
-> Fetching podspec for `Flutter` from `Flutter`
-> Fetching podspec for `$fakePluginName` from `.symlinks/plugins/$fakePluginName/$subdir`
-> Fetching podspec for `another_plugin` from `.symlinks/plugins/another_plugin/ios`
Resolving dependencies of `Podfile`
CDN: trunk Relative path: CocoaPods-version.yml exists! Returning local because checking is only performed in repo update
[!] CocoaPods could not find compatible versions for pod "$fakePluginName":
In Podfile:
$fakePluginName (from `.symlinks/plugins/$fakePluginName/$subdir`)
Specs satisfying the `$fakePluginName (from `.symlinks/plugins/$fakePluginName/subdir`)` dependency were found, but they required a higher minimum deployment target.''';
}
String _fakeHigherMinimumMacOSVersionPodInstallOutput(String fakePluginName, {String subdir = 'macos'}) {
return '''
Preparing
Analyzing dependencies
Inspecting targets to integrate
Using `ARCHS` setting to build architectures of target `Pods-Runner`: (``)
Using `ARCHS` setting to build architectures of target `Pods-RunnerTests`: (``)
Fetching external sources
-> Fetching podspec for `FlutterMacOS` from `Flutter/ephemeral`
-> Fetching podspec for `$fakePluginName` from `Flutter/ephemeral/.symlinks/plugins/$fakePluginName/$subdir`
-> Fetching podspec for `another_plugin` from `Flutter/ephemeral/.symlinks/plugins/another_plugin/macos`
Resolving dependencies of `Podfile`
CDN: trunk Relative path: CocoaPods-version.yml exists! Returning local because checking is only performed in repo update
[!] CocoaPods could not find compatible versions for pod "$fakePluginName":
In Podfile:
$fakePluginName (from `Flutter/ephemeral/.symlinks/plugins/$fakePluginName/$subdir`)
Specs satisfying the `$fakePluginName (from `Flutter/ephemeral/.symlinks/plugins/$fakePluginName/$subdir`)` dependency were found, but they required a higher minimum deployment target.''';
}
class FakeXcodeProjectInterpreter extends Fake implements XcodeProjectInterpreter {
FakeXcodeProjectInterpreter({this.isInstalled = true, this.buildSettings = const <String, String>{}});
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment