| // 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()); |
| } |
| } |