Unverified Commit dffddf00 authored by Jenn Magder's avatar Jenn Magder Committed by GitHub

Refactor BuildIOSFrameworkCommand with common darwin baseclass (#105194)

parent 8dbc3884
......@@ -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'
......
......@@ -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<Set<DevelopmentArtifact>> get requiredArtifacts async => const <DevelopmentArtifact>{
DevelopmentArtifact.iOS,
};
late final FlutterProject _project = FlutterProject.current();
@protected
late final FlutterProject project = FlutterProject.current();
Future<List<BuildInfo>> getBuildInfos() async {
final List<BuildInfo> buildInfos = <BuildInfo>[];
......@@ -143,7 +122,7 @@ class BuildIOSFrameworkCommand extends BuildSubCommand {
}
@override
bool get supported => _platform.isMacOS;
bool get supported => platform.isMacOS;
@override
Future<void> 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<void> produceXCFramework(
Iterable<Directory> frameworks,
String frameworkBinaryName,
Directory outputDirectory,
ProcessManager processManager,
) async {
final List<String> xcframeworkCommand = <String>[
'xcrun',
'xcodebuild',
'-create-xcframework',
for (Directory framework in frameworks) ...<String>[
'-framework',
framework.path,
...framework.parent
.listSync()
.where((FileSystemEntity entity) =>
entity.basename.endsWith('bcsymbolmap') || entity.basename.endsWith('dSYM'))
.map((FileSystemEntity entity) => <String>['-debug-symbols', entity.path])
.expand<String>((List<String> 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<Set<DevelopmentArtifact>> get requiredArtifacts async => const <DevelopmentArtifact>{
DevelopmentArtifact.iOS,
};
@override
Future<void> validateCommand() async {
await super.validateCommand();
if (boolArgDeprecated('universal')) {
throwToolExit('--universal has been deprecated, only XCFrameworks are supported.');
}
}
@override
Future<FlutterCommandResult> 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<BuildInfo> 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: <String, String>{
......@@ -401,7 +460,12 @@ end
status.stop();
}
await _produceXCFramework(frameworks, 'App', outputDirectory);
await BuildFrameworkCommand.produceXCFramework(
frameworks,
'App',
outputDirectory,
globals.processManager,
);
}
Future<void> _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<void> _produceXCFramework(Iterable<Directory> frameworks,
String frameworkBinaryName, Directory outputDirectory) async {
if (!boolArgDeprecated('xcframework')) {
return;
}
final List<String> xcframeworkCommand = <String>[
...globals.xcode!.xcrunCommand(),
'xcodebuild',
'-create-xcframework',
for (Directory framework in frameworks) ...<String>[
'-framework',
framework.path,
...framework.parent
.listSync()
.where((FileSystemEntity entity) =>
entity.basename.endsWith('bcsymbolmap') ||
entity.basename.endsWith('dSYM'))
.map((FileSystemEntity entity) =>
<String>['-debug-symbols', entity.path])
.expand<String>((List<String> 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}');
}
}
}
......@@ -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: <String>[
'xcrun',
'xcodebuild',
'-create-xcframework',
'-framework',
frameworkA.path,
'-framework',
frameworkB.path,
'-output',
output.childDirectory('Combine.xcframework').path,
],
));
await BuildFrameworkCommand.produceXCFramework(
<Directory>[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: <String>[
'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(
<Directory>[frameworkA, frameworkB],
'Combine',
output,
fakeProcessManager,
);
expect(fakeProcessManager.hasRemainingExpectations, isFalse);
});
});
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment