// 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 'package:collection/collection.dart'; import '../base/common.dart'; import '../base/config.dart'; import '../base/error_handling_io.dart'; import '../base/file_system.dart'; import '../base/process.dart'; import '../base/template.dart'; import '../base/version.dart'; import '../darwin/darwin.dart'; import '../plugins.dart'; import '../xcode_project.dart'; import 'swift_packages.dart'; /// The name of the Swift package that's generated by the Flutter tool to add /// dependencies on Flutter plugin swift packages. const kFlutterGeneratedPluginSwiftPackageName = 'FlutterGeneratedPluginSwiftPackage'; /// The name of the Swift pacakge that's generated by the Flutter tool to add /// a dependency on the Flutter/FlutterMacOS framework. const kFlutterGeneratedFrameworkSwiftPackageTargetName = 'FlutterFramework'; const kDisableSwiftPMInstructions = 'You can also disable Swift Package Manager for the project by following these instructions:\n' ' https://docs.flutter.dev/packages-and-plugins/swift-package-manager/for-app-developers#how-to-turn-off-swift-package-manager\n' 'Disabling Swift Package Manager will not be allowed in a future version of Flutter.\n'; /// Swift Package Manager is a dependency management solution for iOS and macOS /// applications. /// /// See also: /// * https://www.swift.org/documentation/package-manager/ - documentation on /// Swift Package Manager. /// * https://developer.apple.com/documentation/packagedescription/package - /// documentation on Swift Package Manager manifest file, Package.swift. class SwiftPackageManager { const SwiftPackageManager({ required FileSystem fileSystem, required TemplateRenderer templateRenderer, required ProcessUtils processUtils, required Config config, }) : _fileSystem = fileSystem, _templateRenderer = templateRenderer, _processUtils = processUtils, _config = config; final FileSystem _fileSystem; final TemplateRenderer _templateRenderer; final ProcessUtils _processUtils; final Config _config; /// Creates a Swift Package called 'FlutterGeneratedPluginSwiftPackage' that /// has dependencies on Flutter plugins that are compatible with Swift /// Package Manager. Future generatePluginsSwiftPackage( List plugins, FlutterDarwinPlatform platform, XcodeBasedProject project, { bool flutterAsADependency = true, }) async { final Directory symlinkDirectory = project.relativeSwiftPackagesDirectory; ErrorHandlingFileSystem.deleteIfExists(symlinkDirectory, recursive: true); symlinkDirectory.createSync(recursive: true); final ( List packageDependencies, List targetDependencies, ) = _dependenciesForPlugins( plugins: plugins, platform: platform, symlinkDirectory: symlinkDirectory, pathRelativeTo: project.flutterPluginSwiftPackageDirectory.path, ); // If there aren't any Swift Package plugins and the project hasn't been // migrated yet, don't generate a Swift package or migrate the app since // it's not needed. If the project has already been migrated, regenerate // the Package.swift even if there are no dependencies in case there // were dependencies previously. if (packageDependencies.isEmpty && !project.flutterPluginSwiftPackageInProjectSettings) { return; } // Add Flutter framework Swift package dependency if (flutterAsADependency) { final ( SwiftPackagePackageDependency flutterFrameworkPackageDependency, SwiftPackageTargetDependency flutterFrameworkTargetDependency, ) = _dependencyForFlutterFramework( pathRelativeTo: project.flutterPluginSwiftPackageDirectory.path, platform: platform, project: project, ); packageDependencies.add(flutterFrameworkPackageDependency); targetDependencies.add(flutterFrameworkTargetDependency); } // FlutterGeneratedPluginSwiftPackage must be statically linked to ensure // any dynamic dependencies are linked to Runner and prevent undefined symbols. final generatedProduct = SwiftPackageProduct.library( name: kFlutterGeneratedPluginSwiftPackageName, targets: [kFlutterGeneratedPluginSwiftPackageName], libraryType: SwiftPackageLibraryType.static, ); final generatedTarget = SwiftPackageTarget.defaultTarget( name: kFlutterGeneratedPluginSwiftPackageName, dependencies: targetDependencies, ); final pluginsPackage = SwiftPackage( manifest: project.flutterPluginSwiftPackageManifest, name: kFlutterGeneratedPluginSwiftPackageName, platforms: [platform.supportedPackagePlatform], products: [generatedProduct], dependencies: packageDependencies, targets: [generatedTarget], templateRenderer: _templateRenderer, ); pluginsPackage.createSwiftPackage(); } (List, List) _dependenciesForPlugins({ required List plugins, required FlutterDarwinPlatform platform, required Directory symlinkDirectory, required String pathRelativeTo, }) { final packageDependencies = []; final targetDependencies = []; for (final plugin in plugins) { final String? pluginSwiftPackageManifestPath = plugin.pluginSwiftPackageManifestPath( _fileSystem, platform.name, ); String? packagePath = plugin.pluginSwiftPackagePath(_fileSystem, platform.name); final File? manifest = packagePath != null ? _fileSystem.file(pluginSwiftPackageManifestPath) : null; if (plugin.platforms[platform.name] == null || packagePath == null || manifest == null || !manifest.existsSync()) { continue; } // Use the plugin basename as the symlink plugin directory name since the basename has the // version number in it. This will make the symlink name change when the plugin version // changes, which forces Xcode to re-process the package manifest. final String basename = _fileSystem.directory(plugin.path).basename; // Check if the plugin has a dependency on another Flutter plugin. // If the plugin has a dependency on another plugin, copy the plugin to the SourcePackages // cache directory and update the manifest to use the versioned path. final String manifestContent = manifest.readAsStringSync(); final List<({String original, String replacement})> pluginDependencies = _getPluginDependencies(manifestContent, plugins); if (pluginDependencies.isNotEmpty) { packagePath = _copyPluginAndUpdateManifest( plugin: plugin, pluginDependencies: pluginDependencies, pathRelativeTo: pathRelativeTo, basename: basename, platform: platform, manifestContent: manifestContent, ); } final Link pluginSymlink = symlinkDirectory.childLink(basename); _createPluginSymlink(pluginSymlink: pluginSymlink, packagePath: packagePath); final String packageRelativePath = _fileSystem.path.relative( pluginSymlink.path, from: pathRelativeTo, ); packageDependencies.add( SwiftPackagePackageDependency(name: plugin.name, path: packageRelativePath), ); // The target dependency product name is hyphen separated because it's // the dependency's library name, which Swift Package Manager will // automatically use as the CFBundleIdentifier if linked dynamically. The // CFBundleIdentifier cannot contain underscores. targetDependencies.add( SwiftPackageTargetDependency.product( name: plugin.name.replaceAll('_', '-'), packageName: plugin.name, ), ); } return (packageDependencies, targetDependencies); } /// Safely creates a symlink at [pluginSymlink] pointing to [packagePath]. /// /// If a symlink already exists and points to the correct target, creation is skipped /// to avoid potential Xcode parallel target build race conditions. /// If creation fails due to sharing violations or locks (e.g., when Xcode is open), /// throws a descriptive [ToolExit] advising the user to close Xcode and run "flutter clean". void _createPluginSymlink({required Link pluginSymlink, required String packagePath}) { final FileSystemEntityType type = _fileSystem.typeSync(pluginSymlink.path, followLinks: false); var skipCreation = false; if (type == FileSystemEntityType.link) { try { if (pluginSymlink.targetSync() == packagePath) { skipCreation = true; } } on FileSystemException catch (_) { // If targetSync fails (e.g. broken link), proceed to delete. } } if (skipCreation) { return; } ErrorHandlingFileSystem.deleteIfExists(pluginSymlink, recursive: true); try { pluginSymlink.createSync(packagePath); } on FileSystemException catch (e) { if (e.osError?.errorCode == 17) { // OS Error: File exists, errno = 17 final FileSystemEntityType postCrashType = _fileSystem.typeSync( pluginSymlink.path, followLinks: false, ); if (postCrashType == FileSystemEntityType.link) { try { if (pluginSymlink.targetSync() == packagePath) { // Concurrently created by another parallel target build, and points to the correct target. return; } } on FileSystemException catch (_) {} } } throwToolExit( 'Failed to create Swift Package plugin symlink at "${pluginSymlink.path}" to "$packagePath":\n' '$e\n' 'If Xcode is currently open, please close Xcode, run "flutter clean", and try building again.', ); } } /// Checks if the plugin has a dependency on another Flutter plugin and returns a list of paths /// that should be replaced in the [manifestContent]. /// /// Plugins can declare a SwiftPM dependency on another plugin like this: /// ```swift /// dependencies: [ /// .package(name: "plugin_1", path: "../plugin_1") /// ] /// ``` /// /// However, plugins are symlinked in the [XcodeBasedProject.relativeSwiftPackagesDirectory] /// using the plugin's basename as the symlink name. The basename for non-path dependencies /// includes the version number, e.g. "plugin_1-1.0.0". To make the relative path in the /// manifest match the symlink path, we need to replace the path in the manifest with the /// symlink path. /// /// For example, the manifest would need to updated to: /// ```swift /// dependencies: [ /// .package(name: "plugin_1", path: "../plugin_1-1.0.0") /// ] /// ``` List<({String original, String replacement})> _getPluginDependencies( String manifestContent, List plugins, ) { final dependencyPattern = RegExp(r'"\.\.\/([^"]+)"'); final Iterable matches = dependencyPattern.allMatches(manifestContent); final List<({String original, String replacement})> pluginDependencies = []; if (matches.isNotEmpty) { for (final match in matches) { final String? path = match.group(0); final String? name = match.group(1); if (path == null || name == null) { continue; } final Plugin? pluginDependency = plugins.firstWhereOrNull((plugin) => plugin.name == name); if (pluginDependency == null) { continue; } final newPath = '"../${_fileSystem.directory(pluginDependency.path).basename}"'; if (path == newPath) { continue; } pluginDependencies.add((original: path, replacement: newPath)); } } return pluginDependencies; } /// Copy the [plugin] to the build directory and update the manifest to use the versioned path. /// Returns the path to the copied plugin. /// /// Plugin must be copied first so that the original plugin in pub cache is not modified. /// /// Throws a [ToolExit] if the plugin cannot be copied or the manifest cannot be updated. String _copyPluginAndUpdateManifest({ required Plugin plugin, required List<({String original, String replacement})> pluginDependencies, required String pathRelativeTo, required String basename, required FlutterDarwinPlatform platform, required String manifestContent, }) { final String destination = _fileSystem .directory( _fileSystem.path.join( platform.buildDirectory(config: _config, fileSystem: _fileSystem), 'SourcePackages', basename, ), ) .absolute .path; final RunResult result = _processUtils.runSync([ 'rsync', '-8', // Avoid mangling filenames with encodings that do not match the current locale. '-av', // Archive mode and verbose: preserve permissions, ownership, timestamps, etc. '--delete', // Delete files in the destination that are not in the source. plugin.path, destination, ]); if (result.exitCode != 0) { throwToolExit('Failed to copy plugin ${plugin.name}: \n${result.stdout}\n${result.stderr}'); } final String? packagePath = plugin.pluginSwiftPackagePath( _fileSystem, platform.name, overridePath: destination, ); if (packagePath == null) { throwToolExit('Failed to find path to Package.swift for plugin ${plugin.name}'); } final File copiedManifest = _fileSystem.directory(packagePath).childFile('Package.swift'); if (!copiedManifest.existsSync()) { throwToolExit( 'Failed to find path to copied Package.swift at ${copiedManifest.path}:\n' 'rsync stdout: \n${result.stdout}\nrsync stderr: \n${result.stderr}', ); } var newManifestContent = manifestContent; for (final dependency in pluginDependencies) { newManifestContent = newManifestContent.replaceAll( dependency.original, dependency.replacement, ); } copiedManifest.writeAsStringSync(newManifestContent); return packagePath; } /// Returns Flutter framework dependencies for the `FlutterGeneratedPluginSwiftPackage`. (SwiftPackagePackageDependency, SwiftPackageTargetDependency) _dependencyForFlutterFramework({ required String pathRelativeTo, required FlutterDarwinPlatform platform, required XcodeBasedProject project, }) { createFlutterFrameworkSwiftPackage(platform: platform, project: project); return ( SwiftPackagePackageDependency( name: kFlutterGeneratedFrameworkSwiftPackageTargetName, path: _fileSystem.path.relative( project.flutterFrameworkSwiftPackageDirectory.path, from: pathRelativeTo, ), ), SwiftPackageTargetDependency.product( name: kFlutterGeneratedFrameworkSwiftPackageTargetName, packageName: kFlutterGeneratedFrameworkSwiftPackageTargetName, ), ); } /// Creates a Swift package called [kFlutterGeneratedFrameworkSwiftPackageTargetName] that vends the /// Flutter/FlutterMacOS framework as a binary target. The Flutter framework is symlinked within /// the package since binary targets must be relative. void createFlutterFrameworkSwiftPackage({ required XcodeBasedProject project, required FlutterDarwinPlatform platform, }) { final flutterFrameworkPackage = SwiftPackage( manifest: project.flutterFrameworkSwiftPackageDirectory.childFile('Package.swift'), name: kFlutterGeneratedFrameworkSwiftPackageTargetName, platforms: [], products: [ SwiftPackageProduct.library( name: kFlutterGeneratedFrameworkSwiftPackageTargetName, targets: [kFlutterGeneratedFrameworkSwiftPackageTargetName], ), ], dependencies: [], targets: [ SwiftPackageTarget.defaultTarget( name: kFlutterGeneratedFrameworkSwiftPackageTargetName, dependencies: [], ), ], templateRenderer: _templateRenderer, ); flutterFrameworkPackage.createSwiftPackage(); } /// If the project's IPHONEOS_DEPLOYMENT_TARGET/MACOSX_DEPLOYMENT_TARGET is /// higher than the FlutterGeneratedPluginSwiftPackage's default /// SupportedPlatform, increase the SupportedPlatform to match the project's /// deployment target. /// /// This is done for the use case of a plugin requiring a higher iOS/macOS /// version than FlutterGeneratedPluginSwiftPackage. /// /// Swift Package Manager emits an error if a dependency isn’t compatible /// with the top-level package’s deployment version. The deployment target of /// a package’s dependencies must be lower than or equal to the top-level /// package’s deployment target version for a particular platform. /// /// To still be able to use the plugin, the user can increase the Xcode /// project's iOS/macOS deployment target and this will then increase the /// deployment target for FlutterGeneratedPluginSwiftPackage. static void updateMinimumDeployment({ required XcodeBasedProject project, required FlutterDarwinPlatform platform, required String deploymentTarget, }) { final Version? projectDeploymentTargetVersion = Version.parse(deploymentTarget); final SwiftPackageSupportedPlatform defaultPlatform = platform.supportedPackagePlatform; final SwiftPackagePlatform packagePlatform = platform.swiftPackagePlatform; if (projectDeploymentTargetVersion == null || projectDeploymentTargetVersion <= defaultPlatform.version || !project.flutterPluginSwiftPackageManifest.existsSync()) { return; } final String manifestContents = project.flutterPluginSwiftPackageManifest.readAsStringSync(); final String oldSupportedPlatform = defaultPlatform.format(); final String newSupportedPlatform = SwiftPackageSupportedPlatform( platform: packagePlatform, version: projectDeploymentTargetVersion, ).format(); project.flutterPluginSwiftPackageManifest.writeAsStringSync( manifestContents.replaceFirst(oldSupportedPlatform, newSupportedPlatform), ); } }