| // 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 '../base/file_system.dart'; | |
| import '../base/template.dart'; | |
| import '../base/version.dart'; | |
| /// Swift toolchain version included with Xcode 15.0. | |
| const minimumSwiftToolchainVersion = '5.9'; | |
| const _swiftPackageTemplate = ''' | |
| // swift-tools-version: {{swiftToolsVersion}} | |
| // The swift-tools-version declares the minimum version of Swift required to build this package. | |
| // | |
| // Generated file. Do not edit. | |
| // | |
| import PackageDescription | |
| {{#hasSwiftCodeBefore}} | |
| {{swiftCodeBefore}} | |
| {{/hasSwiftCodeBefore}} | |
| let package = Package( | |
| name: "{{packageName}}", | |
| {{#platforms}} | |
| platforms: [ | |
| {{platforms}} | |
| ], | |
| {{/platforms}} | |
| products: [ | |
| {{products}} | |
| ], | |
| dependencies: [ | |
| {{dependencies}} | |
| ], | |
| targets: [ | |
| {{targets}} | |
| ] | |
| ) | |
| '''; | |
| const _swiftPackageSourceTemplate = ''' | |
| // | |
| // Generated file. Do not edit. | |
| // | |
| '''; | |
| const _singleIndent = ' '; | |
| const _doubleIndent = '$_singleIndent$_singleIndent'; | |
| /// A Swift Package is reusable code that can be shared across projects and | |
| /// with other developers in iOS and macOS applications. A Swift Package | |
| /// requires a Package.swift. This class handles the formatting and creation of | |
| /// a Package.swift. | |
| /// | |
| /// See https://developer.apple.com/documentation/packagedescription/package | |
| /// for more information about Swift Packages and Package.swift. | |
| class SwiftPackage { | |
| SwiftPackage({ | |
| required this._manifest, | |
| required this._name, | |
| required this._platforms, | |
| required this._products, | |
| required this._dependencies, | |
| required this._targets, | |
| required this._templateRenderer, | |
| this._swiftCodeBeforePackageDefinition, | |
| }); | |
| /// [File] for Package.swift. | |
| final File _manifest; | |
| /// The name of the Swift package. | |
| final String _name; | |
| /// The list of minimum versions for platforms supported by the package. | |
| final List<SwiftPackageSupportedPlatform> _platforms; | |
| /// The list of products that this package vends and that clients can use. | |
| final List<SwiftPackageProduct> _products; | |
| /// The list of package dependencies. | |
| final List<SwiftPackagePackageDependency> _dependencies; | |
| /// The list of targets that are part of this package. | |
| final List<SwiftPackageTarget> _targets; | |
| final TemplateRenderer _templateRenderer; | |
| final String? _swiftCodeBeforePackageDefinition; | |
| /// Context for the [_swiftPackageTemplate] template. | |
| Map<String, Object> get _templateContext => <String, Object>{ | |
| 'swiftToolsVersion': minimumSwiftToolchainVersion, | |
| 'hasSwiftCodeBefore': _swiftCodeBeforePackageDefinition != null, | |
| 'swiftCodeBefore': _swiftCodeBeforePackageDefinition ?? '', | |
| 'packageName': _name, | |
| // Supported platforms can't be empty, so only include if not null. | |
| 'platforms': _formatPlatforms() ?? false, | |
| 'products': _formatProducts(), | |
| 'dependencies': _formatDependencies(), | |
| 'targets': _formatTargets(), | |
| }; | |
| /// Create a Package.swift using settings from [_templateContext]. | |
| /// | |
| /// When [generateEmptySources] is true (the default), it creates an empty Swift source file since | |
| /// Swift packages require at least one source file per non-binary target. | |
| void createSwiftPackage({bool generateEmptySources = true}) { | |
| for (final SwiftPackageTarget target in _targets) { | |
| if (target.targetType != SwiftPackageTargetType.target) { | |
| continue; | |
| } | |
| final Directory targetDirectory = _manifest.parent | |
| .childDirectory('Sources') | |
| .childDirectory(target.name); | |
| if (generateEmptySources) { | |
| final File requiredSwiftFile = targetDirectory.childFile('${target.name}.swift'); | |
| // Skip creating placeholder sources if sources already exist in the | |
| // target directory to avoid unnecessary file writes during build. | |
| final bool hasSources = | |
| requiredSwiftFile.existsSync() || | |
| (targetDirectory.existsSync() && targetDirectory.listSync().isNotEmpty); | |
| if (!hasSources) { | |
| requiredSwiftFile.createSync(recursive: true); | |
| requiredSwiftFile.writeAsStringSync(_swiftPackageSourceTemplate); | |
| } | |
| } | |
| } | |
| final String renderedTemplate = _templateRenderer.renderString( | |
| _swiftPackageTemplate, | |
| _templateContext, | |
| ); | |
| // Skip writing Package.swift if the existing file content is identical to | |
| // renderedTemplate. Preserving file modification time (mtime) prevents | |
| // Xcode and Swift Package Manager from invalidating caches and re-resolving | |
| // dependencies during parallel builds. | |
| var shouldWrite = true; | |
| try { | |
| if (_manifest.existsSync() && _manifest.readAsStringSync() == renderedTemplate) { | |
| shouldWrite = false; | |
| } | |
| } on FileSystemException { | |
| // If reading fails, write it anyway. | |
| } | |
| if (shouldWrite) { | |
| _manifest.createSync(recursive: true); | |
| _manifest.writeAsStringSync(renderedTemplate); | |
| } | |
| } | |
| String? _formatPlatforms() { | |
| if (_platforms.isEmpty) { | |
| return null; | |
| } | |
| final List<String> platformStrings = _platforms | |
| .map((SwiftPackageSupportedPlatform platform) => platform.format()) | |
| .toList(); | |
| return platformStrings.join(',\n$_doubleIndent'); | |
| } | |
| String _formatProducts() { | |
| if (_products.isEmpty) { | |
| return ''; | |
| } | |
| final List<String> libraries = _products | |
| .map((SwiftPackageProduct product) => product.format()) | |
| .toList(); | |
| return libraries.join(',\n$_doubleIndent'); | |
| } | |
| String _formatDependencies() { | |
| if (_dependencies.isEmpty) { | |
| return ''; | |
| } | |
| final List<String> packages = _dependencies | |
| .map((SwiftPackagePackageDependency dependency) => dependency.format()) | |
| .toList(); | |
| return packages.join(',\n$_doubleIndent'); | |
| } | |
| String _formatTargets() { | |
| if (_targets.isEmpty) { | |
| return ''; | |
| } | |
| final List<String> targetList = _targets | |
| .map((SwiftPackageTarget target) => target.format()) | |
| .toList(); | |
| return targetList.join(',\n$_doubleIndent'); | |
| } | |
| } | |
| enum SwiftPackagePlatform { | |
| ios(displayName: '.iOS'), | |
| macos(displayName: '.macOS'), | |
| tvos(displayName: '.tvOS'), | |
| watchos(displayName: '.watchOS'); | |
| const SwiftPackagePlatform({required this.displayName}); | |
| final String displayName; | |
| } | |
| /// A platform that the Swift package supports. | |
| /// | |
| /// Representation of SupportedPlatform from | |
| /// https://developer.apple.com/documentation/packagedescription/supportedplatform. | |
| class SwiftPackageSupportedPlatform { | |
| SwiftPackageSupportedPlatform({required this.platform, required this.version}); | |
| final SwiftPackagePlatform platform; | |
| final Version version; | |
| String format() { | |
| // platforms: [ | |
| // .macOS("12.0"), | |
| // .iOS("15.0"), | |
| // ], | |
| return '${platform.displayName}("$version")'; | |
| } | |
| static SwiftPackageSupportedPlatform? fromJson(Map<String, Object?> json) { | |
| if (json case { | |
| 'platformName': final String platformName, | |
| 'version': final String versionString, | |
| }) { | |
| final Version? parsedVersion = Version.parse(versionString); | |
| if (parsedVersion != null) { | |
| switch (platformName) { | |
| case 'ios': | |
| return SwiftPackageSupportedPlatform(platform: .ios, version: parsedVersion); | |
| case 'macos': | |
| return SwiftPackageSupportedPlatform(platform: .macos, version: parsedVersion); | |
| } | |
| } | |
| } | |
| return null; | |
| } | |
| } | |
| /// Types of library linking. | |
| /// | |
| /// Representation of Product.Library.LibraryType from | |
| /// https://developer.apple.com/documentation/packagedescription/product/library/librarytype. | |
| enum SwiftPackageLibraryType { | |
| dynamic(name: '.dynamic'), | |
| static(name: '.static'); | |
| const SwiftPackageLibraryType({required this.name}); | |
| final String name; | |
| } | |
| /// Types of package products. | |
| /// | |
| /// Representation of Product from | |
| /// https://developer.apple.com/documentation/packagedescription/product. | |
| enum SwiftPackageProductType { | |
| library(name: '.library'), | |
| executable(name: '.executable'), | |
| plugin(name: '.plugin'); | |
| const SwiftPackageProductType({required this.name}); | |
| final String name; | |
| } | |
| /// An externally visible build artifact that's available to clients of the | |
| /// package. | |
| /// | |
| /// Representation of Product from | |
| /// https://developer.apple.com/documentation/packagedescription/product. | |
| class SwiftPackageProduct { | |
| SwiftPackageProduct.library({required this.name, required this.targets, this.libraryType}) | |
| : productType = SwiftPackageProductType.library; | |
| SwiftPackageProduct.plugin({required this.name, required this.targets}) | |
| : productType = SwiftPackageProductType.plugin, | |
| libraryType = null; | |
| SwiftPackageProduct.executable({required this.name, required this.targets}) | |
| : productType = SwiftPackageProductType.executable, | |
| libraryType = null; | |
| final String name; | |
| final SwiftPackageLibraryType? libraryType; | |
| final List<String> targets; | |
| final SwiftPackageProductType productType; | |
| String format() { | |
| // products: [ | |
| // .library(name: "FlutterGeneratedPluginSwiftPackage", targets: ["FlutterGeneratedPluginSwiftPackage"]), | |
| // .library(name: "FlutterDependenciesPackage", type: .dynamic, targets: ["FlutterDependenciesPackage"]), | |
| // ], | |
| var targetsString = ''; | |
| if (targets.isNotEmpty) { | |
| final List<String> quotedTargets = targets.map((String target) => '"$target"').toList(); | |
| targetsString = ', targets: [${quotedTargets.join(', ')}]'; | |
| } | |
| var libraryTypeString = ''; | |
| if (libraryType != null) { | |
| libraryTypeString = ', type: ${libraryType!.name}'; | |
| } | |
| return '${productType.name}(name: "$name"$libraryTypeString$targetsString)'; | |
| } | |
| } | |
| /// A package dependency of a Swift package. | |
| /// | |
| /// Representation of Package.Dependency from | |
| /// https://developer.apple.com/documentation/packagedescription/package/dependency. | |
| class SwiftPackagePackageDependency { | |
| SwiftPackagePackageDependency({required this.name, required this.path}); | |
| final String name; | |
| final String path; | |
| String format() { | |
| // dependencies: [ | |
| // .package(name: "image_picker_ios", path: "/path/to/packages/image_picker/image_picker_ios/ios/image_picker_ios"), | |
| // ], | |
| final String posixPath = path.replaceAll(r'\', '/'); | |
| return '.package(name: "$name", path: "$posixPath")'; | |
| } | |
| } | |
| /// Type of Target constructor. | |
| /// | |
| /// See https://developer.apple.com/documentation/packagedescription/target for | |
| /// more information. | |
| enum SwiftPackageTargetType { | |
| target(name: '.target'), | |
| binaryTarget(name: '.binaryTarget'), | |
| plugin(name: '.plugin'), | |
| executableTarget(name: '.executableTarget'), | |
| testTarget(name: '.testTarget'); | |
| const SwiftPackageTargetType({required this.name}); | |
| final String name; | |
| } | |
| /// A command capability for a plugin target. | |
| /// | |
| /// Representation of Target.PluginCapability.command from | |
| /// https://developer.apple.com/documentation/packagedescription/target/plugincapability-swift.enum/command(intent:permissions:) | |
| class SwiftPackageCommandCapability { | |
| SwiftPackageCommandCapability({required this.verb, required this.description}); | |
| final String verb; | |
| final String description; | |
| String format() { | |
| // capability: .command( | |
| // intent: .custom(verb: "switch-to-debug", description: "Updates package to use the debug mode"), | |
| // permissions: [ | |
| // .writeToPackageDirectory(reason: "Updates package to use the debug mode"), | |
| // ] | |
| // ), | |
| return ''' | |
| capability: .command( | |
| ${_singleIndent * 4}intent: .custom(verb: "$verb", description: "$description"), | |
| ${_singleIndent * 4}permissions: [ | |
| ${_singleIndent * 5}.writeToPackageDirectory(reason: "$description"), | |
| ${_singleIndent * 4}] | |
| ${_singleIndent * 3})'''; | |
| } | |
| } | |
| /// A building block of a Swift Package that contains a set of source files | |
| /// that Swift Package Manager compiles into a module. | |
| /// | |
| /// Representation of Target from | |
| /// https://developer.apple.com/documentation/packagedescription/target. | |
| class SwiftPackageTarget { | |
| SwiftPackageTarget.defaultTarget({required this.name, this.dependencies, this.path}) | |
| : targetType = SwiftPackageTargetType.target, | |
| commandCapability = null; | |
| SwiftPackageTarget.binaryTarget({required this.name, required String relativePath}) | |
| : path = relativePath, | |
| dependencies = null, | |
| targetType = SwiftPackageTargetType.binaryTarget, | |
| commandCapability = null; | |
| SwiftPackageTarget.executableTarget({required this.name, this.dependencies, this.path}) | |
| : targetType = SwiftPackageTargetType.executableTarget, | |
| commandCapability = null; | |
| SwiftPackageTarget.pluginTarget({ | |
| required this.name, | |
| this.path, | |
| this.dependencies, | |
| required this.commandCapability, | |
| }) : targetType = SwiftPackageTargetType.plugin; | |
| SwiftPackageTarget.testTarget({required this.name, this.dependencies, this.path}) | |
| : targetType = SwiftPackageTargetType.testTarget, | |
| commandCapability = null; | |
| final String name; | |
| final String? path; | |
| final List<SwiftPackageTargetDependency>? dependencies; | |
| final SwiftPackageTargetType targetType; | |
| final SwiftPackageCommandCapability? commandCapability; | |
| String format() { | |
| // targets: [ | |
| // .binaryTarget( | |
| // name: "Flutter", | |
| // path: "Flutter.xcframework" | |
| // ), | |
| // .target( | |
| // name: "FlutterGeneratedPluginSwiftPackage", | |
| // dependencies: [ | |
| // .target(name: "Flutter"), | |
| // .product(name: "image_picker_ios", package: "image_picker_ios") | |
| // ] | |
| // ), | |
| // ] | |
| const String targetIndent = _doubleIndent; | |
| const targetDetailsIndent = '$_doubleIndent$_singleIndent'; | |
| final targetDetails = <String>[]; | |
| final nameString = 'name: "$name"'; | |
| targetDetails.add(nameString); | |
| if (commandCapability != null) { | |
| targetDetails.add(commandCapability!.format()); | |
| } | |
| if (dependencies != null && dependencies!.isNotEmpty) { | |
| final List<String> targetDependencies = dependencies! | |
| .map((SwiftPackageTargetDependency dependency) => dependency.format()) | |
| .toList(); | |
| final dependenciesString = | |
| ''' | |
| dependencies: [ | |
| ${targetDependencies.join(",\n")} | |
| $targetDetailsIndent]'''; | |
| targetDetails.add(dependenciesString); | |
| } | |
| if (path != null) { | |
| final pathString = 'path: "$path"'; | |
| targetDetails.add(pathString); | |
| } | |
| return ''' | |
| ${targetType.name}( | |
| $targetDetailsIndent${targetDetails.join(",\n$targetDetailsIndent")} | |
| $targetIndent)'''; | |
| } | |
| } | |
| /// Type of Target.Dependency constructor. | |
| /// | |
| /// See https://developer.apple.com/documentation/packagedescription/target/dependency | |
| /// for more information. | |
| enum SwiftPackageTargetDependencyType { | |
| product(name: '.product'), | |
| target(name: '.target'); | |
| const SwiftPackageTargetDependencyType({required this.name}); | |
| final String name; | |
| } | |
| /// A dependency for the Target on a product from a package dependency or from | |
| /// another Target in the same package. | |
| /// | |
| /// Representation of Target.Dependency from | |
| /// https://developer.apple.com/documentation/packagedescription/target/dependency. | |
| class SwiftPackageTargetDependency { | |
| SwiftPackageTargetDependency.product({required this.name, required String packageName}) | |
| : package = packageName, | |
| dependencyType = SwiftPackageTargetDependencyType.product; | |
| SwiftPackageTargetDependency.target({required this.name}) | |
| : package = null, | |
| dependencyType = SwiftPackageTargetDependencyType.target; | |
| final String name; | |
| final String? package; | |
| final SwiftPackageTargetDependencyType dependencyType; | |
| String format() { | |
| // dependencies: [ | |
| // .target(name: "Flutter"), | |
| // .product(name: "image_picker_ios", package: "image_picker_ios") | |
| // ] | |
| if (dependencyType == SwiftPackageTargetDependencyType.product) { | |
| return '$_doubleIndent$_doubleIndent${dependencyType.name}(name: "$name", package: "$package")'; | |
| } | |
| return '$_doubleIndent$_doubleIndent${dependencyType.name}(name: "$name")'; | |
| } | |
| } |