blob: 24f0d80f34619524b4c84faf86fc898e40b8b3fc [file] [log] [blame]
// Copyright 2013 The Flutter Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:convert';
import 'dart:io';
import 'package:file/file.dart';
import 'common/core.dart';
import 'common/output_utils.dart';
import 'common/package_looping_command.dart';
import 'common/plugin_utils.dart';
import 'common/repository_package.dart';
const int _exitUnsupportedPlatform = 2;
const int _exitPodNotInstalled = 3;
/// Lint the CocoaPod podspecs and run unit tests.
///
/// See https://guides.cocoapods.org/terminal/commands.html#pod_lib_lint.
class PodspecCheckCommand extends PackageLoopingCommand {
/// Creates an instance of the linter command.
PodspecCheckCommand(
super.packagesDir, {
super.processRunner,
super.platform,
super.gitDir,
});
@override
final String name = 'podspec-check';
@override
List<String> get aliases => <String>['podspec', 'podspecs', 'check-podspec'];
@override
final String description =
'Runs "pod lib lint --quick" on all iOS and macOS plugin podspecs, as well as '
'making sure the podspecs follow repository standards.\n\n'
'This command requires "pod" and "flutter" to be in your path. Runs on macOS only.';
@override
Future<void> initializeRun() async {
if (!platform.isMacOS) {
printError('This command is only supported on macOS');
throw ToolExit(_exitUnsupportedPlatform);
}
final ProcessResult result = await processRunner.run(
'which',
<String>['pod'],
workingDir: packagesDir,
logOnError: true,
);
if (result.exitCode != 0) {
printError('Unable to find "pod". Make sure it is in your path.');
throw ToolExit(_exitPodNotInstalled);
}
}
@override
Future<PackageResult> runForPackage(RepositoryPackage package) async {
final errors = <String>[];
final List<File> podspecs = await _podspecsToLint(package);
if (podspecs.isEmpty) {
return PackageResult.skip('No podspecs.');
}
for (final podspec in podspecs) {
if (!await _lintPodspec(podspec)) {
errors.add(podspec.basename);
}
}
if (await _hasIOSSwiftCode(package)) {
print('iOS Swift code found, checking for search paths settings...');
for (final podspec in podspecs) {
if (_isPodspecMissingSearchPaths(podspec)) {
const workaroundBlock = r'''
s.xcconfig = {
'LIBRARY_SEARCH_PATHS' => '$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)/ $(SDKROOT)/usr/lib/swift',
'LD_RUNPATH_SEARCH_PATHS' => '/usr/lib/swift',
}
''';
final String path = getRelativePosixPath(
podspec,
from: package.directory,
);
printError(
'$path is missing search path configuration. Any iOS '
'plugin implementation that contains Swift implementation code '
'needs to contain the following:\n\n'
'$workaroundBlock\n'
'For more details, see https://github.com/flutter/flutter/issues/118418.',
);
errors.add(podspec.basename);
}
}
}
if ((pluginSupportsPlatform(platformIOS, package) ||
pluginSupportsPlatform(platformMacOS, package)) &&
!podspecs.any(_hasPrivacyManifest)) {
printError(
'No PrivacyInfo.xcprivacy file specified. Please ensure that '
'a privacy manifest is included in the build using '
'`resource_bundles`',
);
errors.add('No privacy manifest');
}
return errors.isEmpty
? PackageResult.success()
: PackageResult.fail(errors);
}
Future<List<File>> _podspecsToLint(RepositoryPackage package) async {
final List<File> podspecs = await getFilesForPackage(package).where((
File entity,
) {
final String filename = entity.basename;
return path.extension(filename) == '.podspec' &&
filename != 'Flutter.podspec' &&
filename != 'FlutterMacOS.podspec' &&
!entity.path.contains('packages/pigeon/platform_tests/');
}).toList();
podspecs.sort((File a, File b) => a.basename.compareTo(b.basename));
return podspecs;
}
Future<bool> _lintPodspec(File podspec) async {
print('Linting ${podspec.basename}');
final ProcessResult lintResult = await _runPodLint(podspec.path);
print(lintResult.stdout);
print(lintResult.stderr);
return lintResult.exitCode == 0;
}
Future<ProcessResult> _runPodLint(String podspecPath) async {
final arguments = <String>['lib', 'lint', podspecPath, '--quick'];
print('Running "pod ${arguments.join(' ')}"');
return processRunner.run(
'pod',
arguments,
workingDir: packagesDir,
stdoutEncoding: utf8,
stderrEncoding: utf8,
);
}
/// Returns true if there is any iOS plugin implementation code written in
/// Swift. Skips files named "Package.swift", which is a Swift Package Manager
/// manifest file and does not mean the plugin is written in Swift.
Future<bool> _hasIOSSwiftCode(RepositoryPackage package) async {
final String iosSwiftPackageManifestPath = package
.platformDirectory(FlutterPlatform.ios)
.childDirectory(package.directory.basename)
.childFile('Package.swift')
.path;
final String darwinSwiftPackageManifestPath = package.directory
.childDirectory('darwin')
.childDirectory(package.directory.basename)
.childFile('Package.swift')
.path;
return getFilesForPackage(package).any((File entity) {
final String relativePath = getRelativePosixPath(
entity,
from: package.directory,
);
// Ignore example code.
if (relativePath.startsWith('example/')) {
return false;
}
// Ignore test code.
if (relativePath.contains('/Tests/') ||
relativePath.contains('/RunnerTests/') ||
relativePath.contains('/RunnerUITests/')) {
return false;
}
final String filePath = entity.path;
return filePath != iosSwiftPackageManifestPath &&
filePath != darwinSwiftPackageManifestPath &&
path.extension(filePath) == '.swift';
});
}
/// Returns true if [podspec] could apply to iOS, but does not have the
/// workaround for search paths that makes Swift plugins build correctly in
/// Objective-C applications. See
/// https://github.com/flutter/flutter/issues/118418 for context and details.
///
/// This does not check that the plugin has Swift code, and thus whether the
/// workaround is needed, only whether or not it is there.
bool _isPodspecMissingSearchPaths(File podspec) {
final String directory = podspec.parent.basename;
// All macOS Flutter apps are Swift, so macOS-only podspecs don't need the
// workaround. If it's anywhere other than macos/, err or the side of
// assuming it's required.
if (directory == 'macos') {
return false;
}
// This errs on the side of being too strict, to minimize the chance of
// accidental incorrect configuration. If we ever need more flexibility
// due to a false negative we can adjust this as necessary.
final workaround = RegExp(r'''
\s*s\.(?:ios\.)?xcconfig = {[^}]*
\s*'LIBRARY_SEARCH_PATHS' => '\$\(TOOLCHAIN_DIR\)/usr/lib/swift/\$\(PLATFORM_NAME\)/ \$\(SDKROOT\)/usr/lib/swift',
\s*'LD_RUNPATH_SEARCH_PATHS' => '/usr/lib/swift',[^}]*
\s*}''', dotAll: true);
return !workaround.hasMatch(podspec.readAsStringSync());
}
/// Returns true if [podspec] specifies a .xcprivacy file.
bool _hasPrivacyManifest(File podspec) {
final manifestBundling = RegExp(
r'''
\.(?:ios\.)?resource_bundles\s*=\s*{[^}]*PrivacyInfo.xcprivacy''',
dotAll: true,
);
return manifestBundling.hasMatch(podspec.readAsStringSync());
}
}