diff --git a/dev/devicelab/bin/tasks/build_ios_framework_module_test.dart b/dev/devicelab/bin/tasks/build_ios_framework_module_test.dart index 655764fcf17..fc9c07f32f7 100644 --- a/dev/devicelab/bin/tasks/build_ios_framework_module_test.dart +++ b/dev/devicelab/bin/tasks/build_ios_framework_module_test.dart @@ -151,7 +151,7 @@ Future _testBuildIosFramework(Directory projectDir, { bool isModule = fals section('Check debug build has no Dart AOT'); - final String aotSymbols = await _dylibSymbols(debugAppFrameworkPath); + final String aotSymbols = await dumpSymbolTable(debugAppFrameworkPath); if (aotSymbols.contains('architecture') || aotSymbols.contains('_kDartVmSnapshot')) { @@ -172,7 +172,7 @@ Future _testBuildIosFramework(Directory projectDir, { bool isModule = fals await _checkDylib(appFrameworkPath); - final String aotSymbols = await _dylibSymbols(appFrameworkPath); + final String aotSymbols = await dumpSymbolTable(appFrameworkPath); if (!aotSymbols.contains('_kDartVmSnapshot')) { throw TaskResult.failure('$mode App.framework missing Dart AOT'); @@ -562,7 +562,7 @@ Future _testBuildMacOSFramework(Directory projectDir) async { section('Check debug build has no Dart AOT'); - final String aotSymbols = await _dylibSymbols(debugAppFrameworkPath); + final String aotSymbols = await dumpSymbolTable(debugAppFrameworkPath); if (aotSymbols.contains('architecture') || aotSymbols.contains('_kDartVmSnapshot')) { @@ -583,7 +583,7 @@ Future _testBuildMacOSFramework(Directory projectDir) async { await _checkDylib(appFrameworkPath); - final String aotSymbols = await _dylibSymbols(appFrameworkPath); + final String aotSymbols = await dumpSymbolTable(appFrameworkPath); if (!aotSymbols.contains('_kDartVmSnapshot')) { throw TaskResult.failure('$mode App.framework missing Dart AOT'); @@ -939,15 +939,6 @@ Future _checkStatic(String pathToLibrary) async { } } -Future _dylibSymbols(String pathToDylib) { - return eval('nm', [ - '-g', - pathToDylib, - '-arch', - 'arm64', - ]); -} - Future _linksOnFlutter(String pathToBinary) async { final String loadCommands = await eval('otool', [ '-l', diff --git a/dev/devicelab/bin/tasks/module_test_ios.dart b/dev/devicelab/bin/tasks/module_test_ios.dart index b45ca54ae63..98865c92360 100644 --- a/dev/devicelab/bin/tasks/module_test_ios.dart +++ b/dev/devicelab/bin/tasks/module_test_ios.dart @@ -703,13 +703,7 @@ Future _isAppAotBuild(Directory app) async { 'App', ); - final String symbolTable = await eval( - 'nm', - [ - '-gU', - binary, - ], - ); + final String symbolTable = await dumpSymbolTable(binary); return symbolTable.contains('kDartIsolateSnapshotInstructions'); } diff --git a/dev/devicelab/bin/tasks/plugin_dependencies_test.dart b/dev/devicelab/bin/tasks/plugin_dependencies_test.dart index bf74614c3c5..baae2f6bf63 100644 --- a/dev/devicelab/bin/tasks/plugin_dependencies_test.dart +++ b/dev/devicelab/bin/tasks/plugin_dependencies_test.dart @@ -6,6 +6,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:flutter_devicelab/framework/framework.dart'; +import 'package:flutter_devicelab/framework/ios.dart'; import 'package:flutter_devicelab/framework/task_result.dart'; import 'package:flutter_devicelab/framework/utils.dart'; import 'package:path/path.dart' as path; @@ -209,6 +210,7 @@ public class DummyPluginAClass { final String flutterPluginsDependenciesFileContent = flutterPluginsDependenciesFile.readAsStringSync(); final Map jsonContent = json.decode(flutterPluginsDependenciesFileContent) as Map; + final bool swiftPackageManagerEnabled = jsonContent['swift_package_manager_enabled'] as bool? ?? false; // Verify the dependencyGraph object is valid. The rest of the contents of this file are not relevant to the // dependency graph and are tested by unit tests. @@ -302,28 +304,35 @@ public class DummyPluginAClass { return TaskResult.failure('Failed to build plugin A example iOS app'); } - checkDirectoryExists(path.join( - appBundle.path, - 'Frameworks', - 'plugin_a.framework', - )); - checkDirectoryExists(path.join( - appBundle.path, - 'Frameworks', - 'plugin_b.framework', - )); - checkDirectoryExists(path.join( - appBundle.path, - 'Frameworks', - 'plugin_c.framework', - )); + if (swiftPackageManagerEnabled) { + // Check plugins are built statically if using SwiftPM. + final String executable = path.join(appBundle.path, 'Runner'); + final String symbols = await dumpSymbolTable(executable); - // Plugin D is Android only and should not be embedded. - checkDirectoryNotExists(path.join( - appBundle.path, - 'Frameworks', - 'plugin_d.framework', - )); + final bool foundA = symbols.contains('plugin_a'); + final bool foundB = symbols.contains('plugin_b'); + final bool foundC = symbols.contains('plugin_c'); + final bool foundD = symbols.contains('plugin_d'); + + if (!foundA || !foundB || !foundC) { + return TaskResult.failure( + 'Failed to find plugins_a, plugin_b, or plugin_c symbols in the app' + ); + } + + if (foundD) { + return TaskResult.failure( + 'Found Android plugin_d symbols in iOS app' + ); + } + } else { + // Check plugins are built dynamically if using CocoaPods. + checkDirectoryExists(path.join(appBundle.path, 'Frameworks', 'plugin_a.framework')); + checkDirectoryExists(path.join(appBundle.path, 'Frameworks', 'plugin_b.framework')); + checkDirectoryExists(path.join(appBundle.path, 'Frameworks', 'plugin_c.framework')); + + checkDirectoryNotExists(path.join(appBundle.path, 'Frameworks', 'plugin_d.framework')); + } } return TaskResult.success(null); diff --git a/dev/devicelab/lib/framework/ios.dart b/dev/devicelab/lib/framework/ios.dart index e50573e9b8f..7fc35606eb7 100644 --- a/dev/devicelab/lib/framework/ios.dart +++ b/dev/devicelab/lib/framework/ios.dart @@ -308,3 +308,17 @@ File? _createDisabledSandboxEntitlementFile( return disabledSandboxEntitlementFile; } + +/// Returns global (external) symbol table entries, delimited by new lines. +Future dumpSymbolTable(String filePath) { + return eval( + 'nm', + [ + '--extern-only', + '--just-symbol-name', + filePath, + '-arch', + 'arm64', + ], + ); +} diff --git a/dev/devicelab/lib/tasks/plugin_tests.dart b/dev/devicelab/lib/tasks/plugin_tests.dart index fe6cb4449bf..c41cb40470b 100644 --- a/dev/devicelab/lib/tasks/plugin_tests.dart +++ b/dev/devicelab/lib/tasks/plugin_tests.dart @@ -77,6 +77,11 @@ class PluginTest { final _FlutterProject app = await _FlutterProject.create(tempDir, options, buildTarget, name: 'plugintestapp', template: 'app', environment: appCreateEnvironment); try { + if (cocoapodsTransitiveFlutterDependency) { + section('Disable Swift Package Manager'); + await app.disableSwiftPackageManager(); + } + section('Add plugins'); await app.addPlugin('plugintest', pluginPath: path.join('..', 'plugintest')); @@ -147,6 +152,20 @@ class _FlutterProject { return _FlutterProject(Directory(path.join(rootPath)), 'example'); } + Future disableSwiftPackageManager() async { + final File pubspec = pubspecFile; + String content = await pubspec.readAsString(); + content = content.replaceFirst( + '# The following section is specific to Flutter packages.\n' + 'flutter:\n', + '# The following section is specific to Flutter packages.\n' + 'flutter:\n' + '\n' + ' disable-swift-package-manager: true\n' + ); + await pubspec.writeAsString(content, flush: true); + } + Future addPlugin(String plugin, {String? pluginPath}) async { final File pubspec = pubspecFile; String content = await pubspec.readAsString(); @@ -244,9 +263,14 @@ class $dartPluginClass { await podspec.writeAsString(podspecContent, flush: true); // Make PlugintestPlugin.swift compile on iOS and macOS with target conditionals. + // If SwiftPM is disabled, the file will be in `darwin/Classes/`. + // Otherwise, the file will be in `darwin//Sources//`. final String pluginClass = '${name[0].toUpperCase()}${name.substring(1)}Plugin'; print('pluginClass: $pluginClass'); - final File pluginRegister = File(path.join(darwinDir.path, 'Classes', '$pluginClass.swift')); + File pluginRegister = File(path.join(darwinDir.path, 'Classes', '$pluginClass.swift')); + if (!pluginRegister.existsSync()) { + pluginRegister = File(path.join(darwinDir.path, name, 'Sources', name, '$pluginClass.swift')); + } final String pluginRegisterContent = ''' #if os(macOS) import FlutterMacOS @@ -494,42 +518,55 @@ s.dependency 'AppAuth', '1.6.0' } if (validateNativeBuildProject) { - final File podsProject = File(path.join(rootPath, target, 'Pods', 'Pods.xcodeproj', 'project.pbxproj')); - if (!podsProject.existsSync()) { - throw TaskResult.failure('Xcode Pods project file missing at ${podsProject.path}'); - } + final File generatedSwiftManifest = File(path.join( + rootPath, + target, + 'Flutter', + 'ephemeral', + 'Packages', + 'FlutterGeneratedPluginSwiftPackage', + 'Package.swift' + )); + final bool swiftPackageManagerEnabled = generatedSwiftManifest.existsSync(); - final String podsProjectContent = podsProject.readAsStringSync(); - if (target == 'ios') { - // Plugins with versions lower than the app version should not have IPHONEOS_DEPLOYMENT_TARGET set. - // The plugintest plugin target should not have IPHONEOS_DEPLOYMENT_TARGET set since it has been lowered - // in _reduceDarwinPluginMinimumVersion to 10, which is below the target version of 11. - if (podsProjectContent.contains('IPHONEOS_DEPLOYMENT_TARGET = 10')) { - throw TaskResult.failure('Plugin build setting IPHONEOS_DEPLOYMENT_TARGET not removed'); + if (!swiftPackageManagerEnabled) { + final File podsProject = File(path.join(rootPath, target, 'Pods', 'Pods.xcodeproj', 'project.pbxproj')); + if (!podsProject.existsSync()) { + throw TaskResult.failure('Xcode Pods project file missing at ${podsProject.path}'); } - // Transitive dependency AppAuth targeting too-low 8.0 was not fixed. - if (podsProjectContent.contains('IPHONEOS_DEPLOYMENT_TARGET = 8')) { - throw TaskResult.failure('Transitive dependency build setting IPHONEOS_DEPLOYMENT_TARGET=8 not removed'); - } - if (!podsProjectContent.contains(r'"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "$(inherited) i386";')) { - throw TaskResult.failure(r'EXCLUDED_ARCHS is not "$(inherited) i386"'); - } - } else if (target == 'macos') { - // Same for macOS deployment target, but 10.8. - // The plugintest target should not have MACOSX_DEPLOYMENT_TARGET set. - if (podsProjectContent.contains('MACOSX_DEPLOYMENT_TARGET = 10.8')) { - throw TaskResult.failure('Plugin build setting MACOSX_DEPLOYMENT_TARGET not removed'); - } - // Transitive dependency AppAuth targeting too-low 10.9 was not fixed. - if (podsProjectContent.contains('MACOSX_DEPLOYMENT_TARGET = 10.9')) { - throw TaskResult.failure('Transitive dependency build setting MACOSX_DEPLOYMENT_TARGET=10.9 not removed'); - } - } - if (localEngine != null) { - final RegExp localEngineSearchPath = RegExp('FRAMEWORK_SEARCH_PATHS\\s*=[^;]*${localEngine.path}'); - if (!localEngineSearchPath.hasMatch(podsProjectContent)) { - throw TaskResult.failure('FRAMEWORK_SEARCH_PATHS does not contain the --local-engine path'); + final String podsProjectContent = podsProject.readAsStringSync(); + if (target == 'ios') { + // Plugins with versions lower than the app version should not have IPHONEOS_DEPLOYMENT_TARGET set. + // The plugintest plugin target should not have IPHONEOS_DEPLOYMENT_TARGET set since it has been lowered + // in _reduceDarwinPluginMinimumVersion to 10, which is below the target version of 11. + if (podsProjectContent.contains('IPHONEOS_DEPLOYMENT_TARGET = 10')) { + throw TaskResult.failure('Plugin build setting IPHONEOS_DEPLOYMENT_TARGET not removed'); + } + // Transitive dependency AppAuth targeting too-low 8.0 was not fixed. + if (podsProjectContent.contains('IPHONEOS_DEPLOYMENT_TARGET = 8')) { + throw TaskResult.failure('Transitive dependency build setting IPHONEOS_DEPLOYMENT_TARGET=8 not removed'); + } + if (!podsProjectContent.contains(r'"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "$(inherited) i386";')) { + throw TaskResult.failure(r'EXCLUDED_ARCHS is not "$(inherited) i386"'); + } + } else if (target == 'macos') { + // Same for macOS deployment target, but 10.8. + // The plugintest target should not have MACOSX_DEPLOYMENT_TARGET set. + if (podsProjectContent.contains('MACOSX_DEPLOYMENT_TARGET = 10.8')) { + throw TaskResult.failure('Plugin build setting MACOSX_DEPLOYMENT_TARGET not removed'); + } + // Transitive dependency AppAuth targeting too-low 10.9 was not fixed. + if (podsProjectContent.contains('MACOSX_DEPLOYMENT_TARGET = 10.9')) { + throw TaskResult.failure('Transitive dependency build setting MACOSX_DEPLOYMENT_TARGET=10.9 not removed'); + } + } + + if (localEngine != null) { + final RegExp localEngineSearchPath = RegExp('FRAMEWORK_SEARCH_PATHS\\s*=[^;]*${localEngine.path}'); + if (!localEngineSearchPath.hasMatch(podsProjectContent)) { + throw TaskResult.failure('FRAMEWORK_SEARCH_PATHS does not contain the --local-engine path'); + } } } } diff --git a/packages/flutter_tools/lib/src/features.dart b/packages/flutter_tools/lib/src/features.dart index 7a8a209f5c7..44e40a9b3d8 100644 --- a/packages/flutter_tools/lib/src/features.dart +++ b/packages/flutter_tools/lib/src/features.dart @@ -186,6 +186,7 @@ const Feature swiftPackageManager = Feature( environmentOverride: 'SWIFT_PACKAGE_MANAGER', master: FeatureChannelSetting( available: true, + enabledByDefault: true, ), ); diff --git a/packages/flutter_tools/test/commands.shard/permeable/create_test.dart b/packages/flutter_tools/test/commands.shard/permeable/create_test.dart index ae42a58736c..11734f1be70 100644 --- a/packages/flutter_tools/test/commands.shard/permeable/create_test.dart +++ b/packages/flutter_tools/test/commands.shard/permeable/create_test.dart @@ -693,7 +693,7 @@ void main() { ), }); - testUsingContext('kotlin/swift plugin project', () async { + testUsingContext('kotlin/swift plugin project without Swift Package Manager', () async { return _createProject( projectDir, ['--no-pub', '--template=plugin', '-a', 'kotlin', '--ios-language', 'swift', '--platforms', 'ios,android'], @@ -718,6 +718,9 @@ void main() { 'ios/Classes/FlutterProjectPlugin.m', ], ); + }, overrides: { + // Test flags disable Swift Package Manager. + FeatureFlags: () => TestFeatureFlags(), }); testUsingContext('swift plugin project with Swift Package Manager', () async { @@ -1944,7 +1947,7 @@ void main() { ); }); - testUsingContext('can re-gen plugin ios/ and example/ folders, reusing custom org', () async { + testUsingContext('can re-gen plugin ios/ and example/ folders, reusing custom org, without Swift Package Manager', () async { await _createProject( projectDir, [ @@ -1969,6 +1972,7 @@ void main() { unexpectedPaths: [ 'example/android/app/src/main/java/com/example/flutter_project_example/MainActivity.java', 'android/src/main/java/com/example/flutter_project/FlutterProjectPlugin.java', + 'ios/flutter_project/Sources/flutter_project/include/flutter_project/FlutterProjectPlugin.h', ], ); final FlutterProject project = FlutterProject.fromDirectory(projectDir); @@ -1976,6 +1980,48 @@ void main() { await project.example.ios.productBundleIdentifier(BuildInfo.debug), 'com.bar.foo.flutterProjectExample', ); + }, overrides: { + // Test flags disable Swift Package Manager. + FeatureFlags: () => TestFeatureFlags(), + }); + + testUsingContext('can re-gen plugin ios/ and example/ folders, reusing custom org, with Swift Package Manager', () async { + await _createProject( + projectDir, + [ + '--no-pub', + '--template=plugin', + '--org', 'com.bar.foo', + '-i', 'objc', + '-a', 'java', + '--platforms', 'ios,android', + ], + [], + ); + projectDir.childDirectory('example').deleteSync(recursive: true); + projectDir.childDirectory('ios').deleteSync(recursive: true); + await _createProject( + projectDir, + ['--no-pub', '--template=plugin', '-i', 'objc', '-a', 'java', '--platforms', 'ios,android'], + [ + 'example/android/app/src/main/java/com/bar/foo/flutter_project_example/MainActivity.java', + 'ios/flutter_project/Sources/flutter_project/include/flutter_project/FlutterProjectPlugin.h', + ], + unexpectedPaths: [ + 'example/android/app/src/main/java/com/example/flutter_project_example/MainActivity.java', + 'android/src/main/java/com/example/flutter_project/FlutterProjectPlugin.java', + 'ios/Classes/FlutterProjectPlugin.h', + ], + ); + final FlutterProject project = FlutterProject.fromDirectory(projectDir); + expect( + await project.example.ios.productBundleIdentifier(BuildInfo.debug), + 'com.bar.foo.flutterProjectExample', + ); + }, overrides: { + FeatureFlags: () => TestFeatureFlags( + isSwiftPackageManagerEnabled: true, + ), }); testUsingContext('fails to re-gen without specified org when org is ambiguous', () async { diff --git a/packages/flutter_tools/test/general.shard/features_test.dart b/packages/flutter_tools/test/general.shard/features_test.dart index 994ae392d6c..423e237ce19 100644 --- a/packages/flutter_tools/test/general.shard/features_test.dart +++ b/packages/flutter_tools/test/general.shard/features_test.dart @@ -404,7 +404,7 @@ void main() { }); test('${swiftPackageManager.name} availability and default enabled', () { - expect(swiftPackageManager.master.enabledByDefault, false); + expect(swiftPackageManager.master.enabledByDefault, true); expect(swiftPackageManager.master.available, true); expect(swiftPackageManager.beta.enabledByDefault, false); expect(swiftPackageManager.beta.available, false); diff --git a/packages/flutter_tools/test/host_cross_arch.shard/ios_content_validation_test.dart b/packages/flutter_tools/test/host_cross_arch.shard/ios_content_validation_test.dart index da5b6a55dbe..f2eeb885bbe 100644 --- a/packages/flutter_tools/test/host_cross_arch.shard/ios_content_validation_test.dart +++ b/packages/flutter_tools/test/host_cross_arch.shard/ios_content_validation_test.dart @@ -87,6 +87,7 @@ void main() { late File outputFlutterFrameworkBinary; late Directory outputAppFramework; late File outputAppFrameworkBinary; + late File outputRunnerBinary; late File outputPluginFrameworkBinary; late Directory buildPath; late Directory buildAppFrameworkDsym; @@ -122,6 +123,10 @@ void main() { outputAppFramework = frameworkDirectory.childDirectory('App.framework'); outputAppFrameworkBinary = outputAppFramework.childFile('App'); + outputRunnerBinary = outputApp.childFile('Runner'); + + // Exists only if the plugin is built as a dynamic framework. + // This is is the default for CocoaPods but not Swift Package Manager. outputPluginFrameworkBinary = frameworkDirectory.childDirectory('hello.framework').childFile('hello'); buildPath = fileSystem.directory(fileSystem.path.join( @@ -141,7 +146,18 @@ void main() { printOnFailure(buildResult.stderr.toString()); expect(buildResult.exitCode, 0); - expect(outputPluginFrameworkBinary, exists); + // Plugins are built either as a static library (SwiftPM's default) + // or as a dynamic library (CocoaPods's default). + // If built as a dynamic library, the plugin will have a .framework. + // If built as static library, the plugin's symbols will be in the + // Runner binary. + final bool helloDynamic = outputPluginFrameworkBinary.existsSync(); + final bool helloStatic = AppleTestUtils + .getExportedSymbols(outputRunnerBinary.path) + .any((String symbol) => symbol.contains('HelloPlugin') && symbol.contains('handle')); + + // Plugin is a dynamic xor static framework. + expect(helloDynamic != helloStatic, isTrue); expect(outputAppFrameworkBinary, exists); expect(outputAppFramework.childFile('Info.plist'), exists); @@ -300,6 +316,19 @@ void main() { ); expect(buildSimulator.exitCode, 0); + // Plugins are built either as a static library (SwiftPM's default) + // or as a dynamic library (CocoaPods's default). + // If built as a dynamic library, the plugin will have a .framework. + // If built as static library, the plugin's symbols will be in the + // Runner binary. + final File runnerBinary = fileSystem.file(fileSystem.path.join( + projectRoot, + 'build', + 'ios', + 'iphonesimulator', + 'Runner.app', + 'Runner', + )); final File pluginFrameworkBinary = fileSystem.file(fileSystem.path.join( projectRoot, 'build', @@ -310,12 +339,21 @@ void main() { 'hello.framework', 'hello', )); - expect(pluginFrameworkBinary, exists); - final ProcessResult archs = processManager.runSync( - ['file', pluginFrameworkBinary.path], - ); - expect(archs.stdout, contains('Mach-O 64-bit dynamically linked shared library x86_64')); - expect(archs.stdout, contains('Mach-O 64-bit dynamically linked shared library arm64')); + final bool helloDynamic = pluginFrameworkBinary.existsSync(); + final bool helloStatic = AppleTestUtils + .getExportedSymbols(runnerBinary.path) + .any((String symbol) => symbol.contains('HelloPlugin') && symbol.contains('handle')); + + // Plugin is a dynamic xor static framework. + expect(helloDynamic != helloStatic, isTrue); + + if (helloDynamic) { + final ProcessResult archs = processManager.runSync( + ['file', pluginFrameworkBinary.path], + ); + expect(archs.stdout, contains('Mach-O 64-bit dynamically linked shared library x86_64')); + expect(archs.stdout, contains('Mach-O 64-bit dynamically linked shared library arm64')); + } }); testWithoutContext('build for simulator with all available architectures', () {