From dffddf00a22e77122d67f4503efef16a46c18ec2 Mon Sep 17 00:00:00 2001 From: Jenn Magder Date: Thu, 2 Jun 2022 10:28:08 -0700 Subject: [PATCH] Refactor BuildIOSFrameworkCommand with common darwin baseclass (#105194) --- packages/flutter_tools/bin/podhelper.rb | 12 +- .../lib/src/commands/build_ios_framework.dart | 219 ++++++++++-------- ....dart => build_darwin_framework_test.dart} | 79 +++++++ 3 files changed, 211 insertions(+), 99 deletions(-) rename packages/flutter_tools/test/commands.shard/hermetic/{build_ios_framework_test.dart => build_darwin_framework_test.dart} (79%) diff --git a/packages/flutter_tools/bin/podhelper.rb b/packages/flutter_tools/bin/podhelper.rb index b76fc953c78..972679c524e 100644 --- a/packages/flutter_tools/bin/podhelper.rb +++ b/packages/flutter_tools/bin/podhelper.rb @@ -177,9 +177,9 @@ def flutter_install_ios_engine_pod(ios_application_path = nil) Pod::Spec.new do |s| s.name = 'Flutter' s.version = '1.0.0' - s.summary = 'High-performance, high-fidelity mobile apps.' - s.homepage = 'https://flutter.io' - s.license = { :type => 'MIT' } + s.summary = 'A UI toolkit for beautiful and fast apps.' + s.homepage = 'https://flutter.dev' + s.license = { :type => 'BSD' } s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } s.source = { :git => 'https://github.com/flutter/engine', :tag => s.version.to_s } s.ios.deployment_target = '11.0' @@ -215,9 +215,9 @@ def flutter_install_macos_engine_pod(mac_application_path = nil) Pod::Spec.new do |s| s.name = 'FlutterMacOS' s.version = '1.0.0' - s.summary = 'High-performance, high-fidelity mobile apps.' - s.homepage = 'https://flutter.io' - s.license = { :type => 'MIT' } + s.summary = 'A UI toolkit for beautiful and fast apps.' + s.homepage = 'https://flutter.dev' + s.license = { :type => 'BSD' } s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } s.source = { :git => 'https://github.com/flutter/engine', :tag => s.version.to_s } s.osx.deployment_target = '10.11' diff --git a/packages/flutter_tools/lib/src/commands/build_ios_framework.dart b/packages/flutter_tools/lib/src/commands/build_ios_framework.dart index ad3f9b38145..08336599215 100644 --- a/packages/flutter_tools/lib/src/commands/build_ios_framework.dart +++ b/packages/flutter_tools/lib/src/commands/build_ios_framework.dart @@ -3,10 +3,12 @@ // found in the LICENSE file. import 'package:meta/meta.dart'; +import 'package:process/process.dart'; import '../artifacts.dart'; import '../base/common.dart'; import '../base/file_system.dart'; +import '../base/io.dart'; import '../base/logger.dart'; import '../base/platform.dart'; import '../base/process.dart'; @@ -23,18 +25,14 @@ import '../runner/flutter_command.dart' show DevelopmentArtifact, FlutterCommand import '../version.dart'; import 'build.dart'; -/// Produces a .framework for integration into a host iOS app. The .framework -/// contains the Flutter engine and framework code as well as plugins. It can -/// be integrated into plain Xcode projects without using or other package -/// managers. -class BuildIOSFrameworkCommand extends BuildSubCommand { - BuildIOSFrameworkCommand({ +abstract class BuildFrameworkCommand extends BuildSubCommand { + BuildFrameworkCommand({ // Instantiating FlutterVersion kicks off networking, so delay until it's needed, but allow test injection. @visibleForTesting FlutterVersion? flutterVersion, required BuildSystem buildSystem, required bool verboseHelp, Cache? cache, - Platform? platform + Platform? platform, }) : _injectedFlutterVersion = flutterVersion, _buildSystem = buildSystem, _injectedCache = cache, @@ -42,7 +40,6 @@ class BuildIOSFrameworkCommand extends BuildSubCommand { super(verboseHelp: verboseHelp) { addTreeShakeIconsFlag(); usesTargetOption(); - usesFlavorOption(); usesPubOption(); usesDartDefineOption(); addSplitDebugInfoOption(); @@ -67,16 +64,6 @@ class BuildIOSFrameworkCommand extends BuildSubCommand { help: 'Whether to produce a framework for the release build configuration. ' 'By default, all build configurations are built.' ) - ..addFlag('universal', - help: '(deprecated) Produce universal frameworks that include all valid architectures.', - hide: !verboseHelp, - ) - ..addFlag('xcframework', - help: 'Produce xcframeworks that include all valid architectures.', - negatable: false, - defaultsTo: true, - hide: !verboseHelp, - ) ..addFlag('cocoapods', help: 'Produce a Flutter.podspec instead of an engine Flutter.xcframework (recommended if host app uses CocoaPods).', ) @@ -96,35 +83,27 @@ class BuildIOSFrameworkCommand extends BuildSubCommand { } final BuildSystem? _buildSystem; + @protected BuildSystem get buildSystem => _buildSystem ?? globals.buildSystem; - Cache get _cache => _injectedCache ?? globals.cache; + @protected + Cache get cache => _injectedCache ?? globals.cache; final Cache? _injectedCache; - Platform get _platform => _injectedPlatform ?? globals.platform; + @protected + Platform get platform => _injectedPlatform ?? globals.platform; final Platform? _injectedPlatform; // FlutterVersion.instance kicks off git processing which can sometimes fail, so don't try it until needed. - FlutterVersion get _flutterVersion => _injectedFlutterVersion ?? globals.flutterVersion; + @protected + FlutterVersion get flutterVersion => _injectedFlutterVersion ?? globals.flutterVersion; final FlutterVersion? _injectedFlutterVersion; @override bool get reportNullSafety => false; - @override - final String name = 'ios-framework'; - - @override - final String description = 'Produces .xcframeworks for a Flutter project ' - 'and its plugins for integration into existing, plain Xcode projects.\n' - 'This can only be run on macOS hosts.'; - - @override - Future> get requiredArtifacts async => const { - DevelopmentArtifact.iOS, - }; - - late final FlutterProject _project = FlutterProject.current(); + @protected + late final FlutterProject project = FlutterProject.current(); Future> getBuildInfos() async { final List buildInfos = []; @@ -143,7 +122,7 @@ class BuildIOSFrameworkCommand extends BuildSubCommand { } @override - bool get supported => _platform.isMacOS; + bool get supported => platform.isMacOS; @override Future validateCommand() async { @@ -152,14 +131,94 @@ class BuildIOSFrameworkCommand extends BuildSubCommand { throwToolExit('Building frameworks for iOS is only supported on the Mac.'); } - if (boolArgDeprecated('universal')) { - throwToolExit('--universal has been deprecated, only XCFrameworks are supported.'); - } if ((await getBuildInfos()).isEmpty) { throwToolExit('At least one of "--debug" or "--profile", or "--release" is required.'); } } + static Future produceXCFramework( + Iterable frameworks, + String frameworkBinaryName, + Directory outputDirectory, + ProcessManager processManager, + ) async { + final List xcframeworkCommand = [ + 'xcrun', + 'xcodebuild', + '-create-xcframework', + for (Directory framework in frameworks) ...[ + '-framework', + framework.path, + ...framework.parent + .listSync() + .where((FileSystemEntity entity) => + entity.basename.endsWith('bcsymbolmap') || entity.basename.endsWith('dSYM')) + .map((FileSystemEntity entity) => ['-debug-symbols', entity.path]) + .expand((List parameter) => parameter), + ], + '-output', + outputDirectory.childDirectory('$frameworkBinaryName.xcframework').path, + ]; + + final ProcessResult xcframeworkResult = await processManager.run( + xcframeworkCommand, + ); + + if (xcframeworkResult.exitCode != 0) { + throwToolExit('Unable to create $frameworkBinaryName.xcframework: ${xcframeworkResult.stderr}'); + } + } +} + +/// Produces a .framework for integration into a host iOS app. The .framework +/// contains the Flutter engine and framework code as well as plugins. It can +/// be integrated into plain Xcode projects without using or other package +/// managers. +class BuildIOSFrameworkCommand extends BuildFrameworkCommand { + BuildIOSFrameworkCommand({ + super.flutterVersion, + required super.buildSystem, + required bool verboseHelp, + super.cache, + super.platform, + }) : super(verboseHelp: verboseHelp) { + usesFlavorOption(); + + argParser + ..addFlag('universal', + help: '(deprecated) Produce universal frameworks that include all valid architectures.', + hide: !verboseHelp, + ) + ..addFlag('xcframework', + help: 'Produce xcframeworks that include all valid architectures.', + negatable: false, + defaultsTo: true, + hide: !verboseHelp, + ); + } + + @override + final String name = 'ios-framework'; + + @override + final String description = 'Produces .xcframeworks for a Flutter project ' + 'and its plugins for integration into existing, plain iOS Xcode projects.\n' + 'This can only be run on macOS hosts.'; + + @override + Future> get requiredArtifacts async => const { + DevelopmentArtifact.iOS, + }; + + @override + Future validateCommand() async { + await super.validateCommand(); + + if (boolArgDeprecated('universal')) { + throwToolExit('--universal has been deprecated, only XCFrameworks are supported.'); + } + } + @override Future runCommand() async { final String outputArgument = stringArgDeprecated('output') @@ -169,7 +228,7 @@ class BuildIOSFrameworkCommand extends BuildSubCommand { throwToolExit('--output is required.'); } - if (!_project.ios.existsSync()) { + if (!project.ios.existsSync()) { throwToolExit('Project does not support iOS'); } @@ -177,7 +236,7 @@ class BuildIOSFrameworkCommand extends BuildSubCommand { final List buildInfos = await getBuildInfos(); displayNullSafetyMode(buildInfos.first); for (final BuildInfo buildInfo in buildInfos) { - final String? productBundleIdentifier = await _project.ios.productBundleIdentifier(buildInfo); + final String? productBundleIdentifier = await project.ios.productBundleIdentifier(buildInfo); globals.printStatus('Building frameworks for $productBundleIdentifier in ${getNameForBuildMode(buildInfo.mode)} mode...'); final String xcodeBuildConfiguration = sentenceCase(getNameForBuildMode(buildInfo.mode)); final Directory modeDirectory = outputDirectory.childDirectory(xcodeBuildConfiguration); @@ -202,9 +261,9 @@ class BuildIOSFrameworkCommand extends BuildSubCommand { buildInfo, modeDirectory, iPhoneBuildOutput, simulatorBuildOutput); // Build and copy plugins. - await processPodsIfNeeded(_project.ios, getIosBuildDirectory(), buildInfo.mode); - if (hasPlugins(_project)) { - await _producePlugins(buildInfo.mode, xcodeBuildConfiguration, iPhoneBuildOutput, simulatorBuildOutput, modeDirectory, outputDirectory); + await processPodsIfNeeded(project.ios, getIosBuildDirectory(), buildInfo.mode); + if (hasPlugins(project)) { + await _producePlugins(buildInfo.mode, xcodeBuildConfiguration, iPhoneBuildOutput, simulatorBuildOutput, modeDirectory); } final Status status = globals.logger.startProgress( @@ -225,12 +284,12 @@ class BuildIOSFrameworkCommand extends BuildSubCommand { globals.printStatus('Frameworks written to ${outputDirectory.path}.'); - if (!_project.isModule && hasPlugins(_project)) { + if (!project.isModule && hasPlugins(project)) { // Apps do not generate a FlutterPluginRegistrant.framework. Users will need // to copy the GeneratedPluginRegistrant class to their project manually. - final File pluginRegistrantHeader = _project.ios.pluginRegistrantHeader; + final File pluginRegistrantHeader = project.ios.pluginRegistrantHeader; final File pluginRegistrantImplementation = - _project.ios.pluginRegistrantImplementation; + project.ios.pluginRegistrantImplementation; pluginRegistrantHeader.copySync( outputDirectory.childFile(pluginRegistrantHeader.basename).path); pluginRegistrantImplementation.copySync(outputDirectory @@ -250,10 +309,10 @@ class BuildIOSFrameworkCommand extends BuildSubCommand { void produceFlutterPodspec(BuildMode mode, Directory modeDirectory, { bool force = false }) { final Status status = globals.logger.startProgress(' ├─Creating Flutter.podspec...'); try { - final GitTagVersion gitTagVersion = _flutterVersion.gitTagVersion; + final GitTagVersion gitTagVersion = flutterVersion.gitTagVersion; if (!force && (gitTagVersion.x == null || gitTagVersion.y == null || gitTagVersion.z == null || gitTagVersion.commits != 0)) { throwToolExit( - '--cocoapods is only supported on the dev, beta, or stable channels. Detected version is ${_flutterVersion.frameworkVersion}'); + '--cocoapods is only supported on the dev, beta, or stable channels. Detected version is ${flutterVersion.frameworkVersion}'); } // Podspecs use semantic versioning, which don't support hotfixes. @@ -262,7 +321,7 @@ class BuildIOSFrameworkCommand extends BuildSubCommand { // new artifacts when the source URL changes. final int minorHotfixVersion = (gitTagVersion.z ?? 0) * 100 + (gitTagVersion.hotfix ?? 0); - final File license = _cache.getLicenseFile(); + final File license = cache.getLicenseFile(); if (!license.existsSync()) { throwToolExit('Could not find license at ${license.path}'); } @@ -272,7 +331,7 @@ class BuildIOSFrameworkCommand extends BuildSubCommand { final String podspecContents = ''' Pod::Spec.new do |s| s.name = 'Flutter' - s.version = '${gitTagVersion.x}.${gitTagVersion.y}.$minorHotfixVersion' # ${_flutterVersion.frameworkVersion} + s.version = '${gitTagVersion.x}.${gitTagVersion.y}.$minorHotfixVersion' # ${flutterVersion.frameworkVersion} s.summary = 'A UI toolkit for beautiful and fast apps.' s.description = <<-DESC Flutter is Google's UI toolkit for building beautiful, fast apps for mobile, web, desktop, and embedded devices from a single codebase. @@ -285,7 +344,7 @@ $licenseSource LICENSE } s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } - s.source = { :http => '${_cache.storageBaseUrl}/flutter_infra_release/flutter/${_cache.engineRevision}/$artifactsMode/artifacts.zip' } + s.source = { :http => '${cache.storageBaseUrl}/flutter_infra_release/flutter/${cache.engineRevision}/$artifactsMode/artifacts.zip' } s.documentation_url = 'https://flutter.dev/docs' s.platform = :ios, '11.0' s.vendored_frameworks = 'Flutter.xcframework' @@ -356,7 +415,7 @@ end final Environment environment = Environment( projectDir: globals.fs.currentDirectory, outputDir: outputBuildDirectory, - buildDir: _project.dartTool.childDirectory('flutter_build'), + buildDir: project.dartTool.childDirectory('flutter_build'), cacheDir: globals.cache.getRoot(), flutterRootDir: globals.fs.directory(Cache.flutterRoot), defines: { @@ -401,7 +460,12 @@ end status.stop(); } - await _produceXCFramework(frameworks, 'App', outputDirectory); + await BuildFrameworkCommand.produceXCFramework( + frameworks, + 'App', + outputDirectory, + globals.processManager, + ); } Future _producePlugins( @@ -410,7 +474,6 @@ end Directory iPhoneBuildOutput, Directory simulatorBuildOutput, Directory modeDirectory, - Directory outputDirectory, ) async { final Status status = globals.logger.startProgress( ' ├─Building plugins...' @@ -437,7 +500,7 @@ end RunResult buildPluginsResult = await globals.processUtils.run( pluginsBuildCommand, - workingDirectory: _project.ios.hostAppRoot.childDirectory('Pods').path, + workingDirectory: project.ios.hostAppRoot.childDirectory('Pods').path, ); if (buildPluginsResult.exitCode != 0) { @@ -464,7 +527,7 @@ end buildPluginsResult = await globals.processUtils.run( pluginsBuildCommand, - workingDirectory: _project.ios.hostAppRoot + workingDirectory: project.ios.hostAppRoot .childDirectory('Pods') .path, ); @@ -500,46 +563,16 @@ end .childDirectory(podFrameworkName), ]; - await _produceXCFramework(frameworks, binaryName, modeDirectory); + await BuildFrameworkCommand.produceXCFramework( + frameworks, + binaryName, + modeDirectory, + globals.processManager, + ); } } } finally { status.stop(); } } - - Future _produceXCFramework(Iterable frameworks, - String frameworkBinaryName, Directory outputDirectory) async { - if (!boolArgDeprecated('xcframework')) { - return; - } - final List xcframeworkCommand = [ - ...globals.xcode!.xcrunCommand(), - 'xcodebuild', - '-create-xcframework', - for (Directory framework in frameworks) ...[ - '-framework', - framework.path, - ...framework.parent - .listSync() - .where((FileSystemEntity entity) => - entity.basename.endsWith('bcsymbolmap') || - entity.basename.endsWith('dSYM')) - .map((FileSystemEntity entity) => - ['-debug-symbols', entity.path]) - .expand((List parameter) => parameter), - ], - '-output', - outputDirectory.childDirectory('$frameworkBinaryName.xcframework').path, - ]; - - final RunResult xcframeworkResult = await globals.processUtils.run( - xcframeworkCommand, - ); - - if (xcframeworkResult.exitCode != 0) { - throwToolExit( - 'Unable to create $frameworkBinaryName.xcframework: ${xcframeworkResult.stderr}'); - } - } } diff --git a/packages/flutter_tools/test/commands.shard/hermetic/build_ios_framework_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/build_darwin_framework_test.dart similarity index 79% rename from packages/flutter_tools/test/commands.shard/hermetic/build_ios_framework_test.dart rename to packages/flutter_tools/test/commands.shard/hermetic/build_darwin_framework_test.dart index 577ec755907..1d4143a69ea 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/build_ios_framework_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/build_darwin_framework_test.dart @@ -271,4 +271,83 @@ void main() { }); }); }); + + group('XCFrameworks', () { + MemoryFileSystem fileSystem; + FakeProcessManager fakeProcessManager; + + setUp(() { + fileSystem = MemoryFileSystem.test(); + fakeProcessManager = FakeProcessManager.empty(); + }); + + testWithoutContext('created', () async { + final Directory frameworkA = fileSystem.directory('FrameworkA.framework')..createSync(); + final Directory frameworkB = fileSystem.directory('FrameworkB.framework')..createSync(); + final Directory output = fileSystem.directory('output'); + + fakeProcessManager.addCommand(FakeCommand( + command: [ + 'xcrun', + 'xcodebuild', + '-create-xcframework', + '-framework', + frameworkA.path, + '-framework', + frameworkB.path, + '-output', + output.childDirectory('Combine.xcframework').path, + ], + )); + await BuildFrameworkCommand.produceXCFramework( + [frameworkA, frameworkB], + 'Combine', + output, + fakeProcessManager, + ); + expect(fakeProcessManager.hasRemainingExpectations, isFalse); + }); + + testWithoutContext('created with symbols', () async { + final Directory parentA = fileSystem.directory('FrameworkA')..createSync(); + final File bcsymbolmapA = parentA.childFile('ABC123.bcsymbolmap')..createSync(); + final File dSYMA = parentA.childFile('FrameworkA.framework.dSYM')..createSync(); + final Directory frameworkA = parentA.childDirectory('FrameworkA.framework')..createSync(); + + final Directory parentB = fileSystem.directory('FrameworkB')..createSync(); + final File bcsymbolmapB = parentB.childFile('ZYX987.bcsymbolmap')..createSync(); + final File dSYMB = parentB.childFile('FrameworkB.framework.dSYM')..createSync(); + final Directory frameworkB = parentB.childDirectory('FrameworkB.framework')..createSync(); + final Directory output = fileSystem.directory('output'); + + fakeProcessManager.addCommand(FakeCommand( + command: [ + 'xcrun', + 'xcodebuild', + '-create-xcframework', + '-framework', + frameworkA.path, + '-debug-symbols', + bcsymbolmapA.path, + '-debug-symbols', + dSYMA.path, + '-framework', + frameworkB.path, + '-debug-symbols', + bcsymbolmapB.path, + '-debug-symbols', + dSYMB.path, + '-output', + output.childDirectory('Combine.xcframework').path, + ], + )); + await BuildFrameworkCommand.produceXCFramework( + [frameworkA, frameworkB], + 'Combine', + output, + fakeProcessManager, + ); + expect(fakeProcessManager.hasRemainingExpectations, isFalse); + }); + }); }