Add flutter build macos-framework command (#105242)
This commit is contained in:
parent
975e04ba6d
commit
3f1f0a8170
@ -10,7 +10,7 @@ import 'package:flutter_devicelab/framework/task_result.dart';
|
||||
import 'package:flutter_devicelab/framework/utils.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
/// Tests that iOS .xcframeworks can be built.
|
||||
/// Tests that iOS and macOS .xcframeworks can be built.
|
||||
Future<void> main() async {
|
||||
await task(() async {
|
||||
|
||||
@ -19,7 +19,7 @@ Future<void> main() async {
|
||||
final Directory tempDir = Directory.systemTemp.createTempSync('flutter_module_test.');
|
||||
try {
|
||||
await inDirectory(tempDir, () async {
|
||||
section('Test module template');
|
||||
section('Test iOS module template');
|
||||
|
||||
final Directory moduleProjectDir =
|
||||
Directory(path.join(tempDir.path, 'hello_module'));
|
||||
@ -34,6 +34,7 @@ Future<void> main() async {
|
||||
],
|
||||
);
|
||||
|
||||
await _addPlugin(moduleProjectDir);
|
||||
await _testBuildIosFramework(moduleProjectDir, isModule: true);
|
||||
|
||||
section('Test app template');
|
||||
@ -45,7 +46,9 @@ Future<void> main() async {
|
||||
options: <String>['--org', 'io.flutter.devicelab', 'hello_project'],
|
||||
);
|
||||
|
||||
await _addPlugin(projectDir);
|
||||
await _testBuildIosFramework(projectDir);
|
||||
await _testBuildMacOSFramework(projectDir);
|
||||
});
|
||||
|
||||
return TaskResult.success(null);
|
||||
@ -59,7 +62,7 @@ Future<void> main() async {
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _testBuildIosFramework(Directory projectDir, { bool isModule = false}) async {
|
||||
Future<void> _addPlugin(Directory projectDir) async {
|
||||
section('Add plugins');
|
||||
|
||||
final File pubspec = File(path.join(projectDir.path, 'pubspec.yaml'));
|
||||
@ -75,24 +78,11 @@ Future<void> _testBuildIosFramework(Directory projectDir, { bool isModule = fals
|
||||
options: <String>['get'],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// First, build the module in Debug to copy the debug version of Flutter.xcframework.
|
||||
// This proves "flutter build ios-framework" re-copies the relevant Flutter.xcframework,
|
||||
// otherwise building plugins with bitcode will fail linking because the debug version
|
||||
// of Flutter.xcframework does not contain bitcode.
|
||||
await inDirectory(projectDir, () async {
|
||||
await flutter(
|
||||
'build',
|
||||
options: <String>[
|
||||
'ios',
|
||||
'--debug',
|
||||
'--no-codesign',
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
Future<void> _testBuildIosFramework(Directory projectDir, { bool isModule = false}) async {
|
||||
// This builds all build modes' frameworks by default
|
||||
section('Build frameworks');
|
||||
section('Build iOS app');
|
||||
|
||||
const String outputDirectoryName = 'flutter-frameworks';
|
||||
|
||||
@ -488,6 +478,293 @@ Future<void> _testBuildIosFramework(Directory projectDir, { bool isModule = fals
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Future<void> _testBuildMacOSFramework(Directory projectDir) async {
|
||||
// This builds all build modes' frameworks by default
|
||||
section('Build macOS frameworks');
|
||||
|
||||
const String outputDirectoryName = 'flutter-frameworks';
|
||||
|
||||
await inDirectory(projectDir, () async {
|
||||
await flutter(
|
||||
'build',
|
||||
options: <String>[
|
||||
'macos-framework',
|
||||
'--verbose',
|
||||
'--output=$outputDirectoryName',
|
||||
'--obfuscate',
|
||||
'--split-debug-info=symbols',
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
final String outputPath = path.join(projectDir.path, outputDirectoryName);
|
||||
final String flutterFramework = path.join(
|
||||
outputPath,
|
||||
'Debug',
|
||||
'FlutterMacOS.xcframework',
|
||||
'macos-arm64_x86_64',
|
||||
'FlutterMacOS.framework',
|
||||
);
|
||||
checkDirectoryExists(flutterFramework);
|
||||
|
||||
final String debugAppFrameworkPath = path.join(
|
||||
outputPath,
|
||||
'Debug',
|
||||
'App.xcframework',
|
||||
'macos-arm64_x86_64',
|
||||
'App.framework',
|
||||
'App',
|
||||
);
|
||||
checkSymlinkExists(debugAppFrameworkPath);
|
||||
|
||||
checkFileExists(path.join(
|
||||
outputPath,
|
||||
'Debug',
|
||||
'App.xcframework',
|
||||
'macos-arm64_x86_64',
|
||||
'App.framework',
|
||||
'Resources',
|
||||
'Info.plist',
|
||||
));
|
||||
|
||||
section('Check debug build has Dart snapshot as asset');
|
||||
|
||||
checkFileExists(path.join(
|
||||
outputPath,
|
||||
'Debug',
|
||||
'App.xcframework',
|
||||
'macos-arm64_x86_64',
|
||||
'App.framework',
|
||||
'Resources',
|
||||
'flutter_assets',
|
||||
'vm_snapshot_data',
|
||||
));
|
||||
|
||||
section('Check obfuscation symbols');
|
||||
|
||||
checkFileExists(path.join(
|
||||
projectDir.path,
|
||||
'symbols',
|
||||
'app.darwin-arm64.symbols',
|
||||
));
|
||||
|
||||
checkFileExists(path.join(
|
||||
projectDir.path,
|
||||
'symbols',
|
||||
'app.darwin-x86_64.symbols',
|
||||
));
|
||||
|
||||
section('Check debug build has no Dart AOT');
|
||||
|
||||
final String aotSymbols = await _dylibSymbols(debugAppFrameworkPath);
|
||||
|
||||
if (aotSymbols.contains('architecture') ||
|
||||
aotSymbols.contains('_kDartVmSnapshot')) {
|
||||
throw TaskResult.failure('Debug App.framework contains AOT');
|
||||
}
|
||||
|
||||
section('Check profile, release builds has Dart AOT dylib');
|
||||
|
||||
for (final String mode in <String>['Profile', 'Release']) {
|
||||
final String appFrameworkPath = path.join(
|
||||
outputPath,
|
||||
mode,
|
||||
'App.xcframework',
|
||||
'macos-arm64_x86_64',
|
||||
'App.framework',
|
||||
'App',
|
||||
);
|
||||
|
||||
await _checkDylib(appFrameworkPath);
|
||||
|
||||
final String aotSymbols = await _dylibSymbols(appFrameworkPath);
|
||||
|
||||
if (!aotSymbols.contains('_kDartVmSnapshot')) {
|
||||
throw TaskResult.failure('$mode App.framework missing Dart AOT');
|
||||
}
|
||||
|
||||
checkFileNotExists(path.join(
|
||||
outputPath,
|
||||
mode,
|
||||
'App.xcframework',
|
||||
'macos-arm64_x86_64',
|
||||
'App.framework',
|
||||
'Resources',
|
||||
'flutter_assets',
|
||||
'vm_snapshot_data',
|
||||
));
|
||||
|
||||
checkFileExists(path.join(
|
||||
outputPath,
|
||||
mode,
|
||||
'App.xcframework',
|
||||
'macos-arm64_x86_64',
|
||||
'App.framework',
|
||||
'Resources',
|
||||
'Info.plist',
|
||||
));
|
||||
}
|
||||
|
||||
section("Check all modes' engine dylib");
|
||||
|
||||
for (final String mode in <String>['Debug', 'Profile', 'Release']) {
|
||||
final String engineBinary = path.join(
|
||||
outputPath,
|
||||
mode,
|
||||
'FlutterMacOS.xcframework',
|
||||
'macos-arm64_x86_64',
|
||||
'FlutterMacOS.framework',
|
||||
'FlutterMacOS',
|
||||
);
|
||||
checkSymlinkExists(engineBinary);
|
||||
|
||||
checkFileExists(path.join(
|
||||
outputPath,
|
||||
mode,
|
||||
'FlutterMacOS.xcframework',
|
||||
'macos-arm64_x86_64',
|
||||
'FlutterMacOS.framework',
|
||||
'Headers',
|
||||
'FlutterMacOS.h',
|
||||
));
|
||||
}
|
||||
|
||||
section('Check all modes have plugins');
|
||||
|
||||
for (final String mode in <String>['Debug', 'Profile', 'Release']) {
|
||||
final String pluginFrameworkPath = path.join(
|
||||
outputPath,
|
||||
mode,
|
||||
'connectivity_macos.xcframework',
|
||||
'macos-arm64_x86_64',
|
||||
'connectivity_macos.framework',
|
||||
'connectivity_macos',
|
||||
);
|
||||
|
||||
await _checkDylib(pluginFrameworkPath);
|
||||
if (!await _linksOnFlutterMacOS(pluginFrameworkPath)) {
|
||||
throw TaskResult.failure('$pluginFrameworkPath does not link on Flutter');
|
||||
}
|
||||
|
||||
final String transitiveDependencyFrameworkPath = path.join(
|
||||
outputPath,
|
||||
mode,
|
||||
'Reachability.xcframework',
|
||||
'macos-arm64_x86_64',
|
||||
'Reachability.framework',
|
||||
'Reachability',
|
||||
);
|
||||
if (await _linksOnFlutterMacOS(transitiveDependencyFrameworkPath)) {
|
||||
throw TaskResult.failure('Transitive dependency $transitiveDependencyFrameworkPath unexpectedly links on Flutter');
|
||||
}
|
||||
|
||||
checkFileExists(path.join(
|
||||
outputPath,
|
||||
mode,
|
||||
'connectivity_macos.xcframework',
|
||||
'macos-arm64_x86_64',
|
||||
'connectivity_macos.framework',
|
||||
'Headers',
|
||||
'connectivity_macos-Swift.h',
|
||||
));
|
||||
|
||||
checkDirectoryExists(path.join(
|
||||
outputPath,
|
||||
mode,
|
||||
'connectivity_macos.xcframework',
|
||||
'macos-arm64_x86_64',
|
||||
'connectivity_macos.framework',
|
||||
'Modules',
|
||||
'connectivity_macos.swiftmodule',
|
||||
));
|
||||
|
||||
if (mode != 'Debug') {
|
||||
checkDirectoryExists(path.join(
|
||||
outputPath,
|
||||
mode,
|
||||
'connectivity_macos.xcframework',
|
||||
'macos-arm64_x86_64',
|
||||
'dSYMs',
|
||||
'connectivity_macos.framework.dSYM',
|
||||
));
|
||||
}
|
||||
|
||||
checkSymlinkExists(path.join(
|
||||
outputPath,
|
||||
mode,
|
||||
'connectivity_macos.xcframework',
|
||||
'macos-arm64_x86_64',
|
||||
'connectivity_macos.framework',
|
||||
'connectivity_macos',
|
||||
));
|
||||
}
|
||||
|
||||
// This builds all build modes' frameworks by default
|
||||
section('Build podspec and static plugins');
|
||||
|
||||
const String cocoapodsOutputDirectoryName = 'flutter-frameworks-cocoapods';
|
||||
|
||||
await inDirectory(projectDir, () async {
|
||||
await flutter(
|
||||
'build',
|
||||
options: <String>[
|
||||
'macos-framework',
|
||||
'--cocoapods',
|
||||
'--force', // Allow podspec creation on master.
|
||||
'--output=$cocoapodsOutputDirectoryName',
|
||||
'--static',
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
final String cocoapodsOutputPath = path.join(projectDir.path, cocoapodsOutputDirectoryName);
|
||||
for (final String mode in <String>['Debug', 'Profile', 'Release']) {
|
||||
checkFileExists(path.join(
|
||||
cocoapodsOutputPath,
|
||||
mode,
|
||||
'FlutterMacOS.podspec',
|
||||
));
|
||||
await _checkDylib(path.join(
|
||||
cocoapodsOutputPath,
|
||||
mode,
|
||||
'App.xcframework',
|
||||
'macos-arm64_x86_64',
|
||||
'App.framework',
|
||||
'App',
|
||||
));
|
||||
|
||||
await _checkStatic(path.join(
|
||||
cocoapodsOutputPath,
|
||||
mode,
|
||||
'package_info.xcframework',
|
||||
'macos-arm64_x86_64',
|
||||
'package_info.framework',
|
||||
'package_info',
|
||||
));
|
||||
|
||||
await _checkStatic(path.join(
|
||||
cocoapodsOutputPath,
|
||||
mode,
|
||||
'connectivity_macos.xcframework',
|
||||
'macos-arm64_x86_64',
|
||||
'connectivity_macos.framework',
|
||||
'connectivity_macos',
|
||||
));
|
||||
|
||||
checkDirectoryExists(path.join(
|
||||
cocoapodsOutputPath,
|
||||
mode,
|
||||
'Reachability.xcframework',
|
||||
));
|
||||
}
|
||||
|
||||
checkFileExists(path.join(
|
||||
outputPath,
|
||||
'GeneratedPluginRegistrant.swift',
|
||||
));
|
||||
}
|
||||
|
||||
Future<void> _checkDylib(String pathToLibrary) async {
|
||||
final String binaryFileType = await fileType(pathToLibrary);
|
||||
if (!binaryFileType.contains('dynamically linked')) {
|
||||
@ -529,3 +806,13 @@ Future<bool> _linksOnFlutter(String pathToBinary) async {
|
||||
]);
|
||||
return loadCommands.contains('Flutter.framework');
|
||||
}
|
||||
|
||||
Future<bool> _linksOnFlutterMacOS(String pathToBinary) async {
|
||||
final String loadCommands = await eval('otool', <String>[
|
||||
'-l',
|
||||
'-arch',
|
||||
'arm64',
|
||||
pathToBinary,
|
||||
]);
|
||||
return loadCommands.contains('FlutterMacOS.framework');
|
||||
}
|
||||
|
@ -753,6 +753,13 @@ void checkDirectoryNotExists(String directory) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks that the symlink exists, otherwise throws a [FileSystemException].
|
||||
void checkSymlinkExists(String file) {
|
||||
if (!exists(Link(file))) {
|
||||
throw FileSystemException('Expected symlink to exist.', file);
|
||||
}
|
||||
}
|
||||
|
||||
/// Check that `collection` contains all entries in `values`.
|
||||
void checkCollectionContains<T>(Iterable<T> values, Iterable<T> collection) {
|
||||
for (final T value in values) {
|
||||
|
@ -17,6 +17,7 @@ import 'build_bundle.dart';
|
||||
import 'build_fuchsia.dart';
|
||||
import 'build_ios.dart';
|
||||
import 'build_ios_framework.dart';
|
||||
import 'build_macos_framework.dart';
|
||||
import 'build_web.dart';
|
||||
|
||||
class BuildCommand extends FlutterCommand {
|
||||
@ -29,6 +30,10 @@ class BuildCommand extends FlutterCommand {
|
||||
buildSystem: globals.buildSystem,
|
||||
verboseHelp: verboseHelp,
|
||||
));
|
||||
_addSubcommand(BuildMacOSFrameworkCommand(
|
||||
buildSystem: globals.buildSystem,
|
||||
verboseHelp: verboseHelp,
|
||||
));
|
||||
_addSubcommand(BuildIOSArchiveCommand(verboseHelp: verboseHelp));
|
||||
_addSubcommand(BuildBundleCommand(verboseHelp: verboseHelp));
|
||||
_addSubcommand(BuildWebCommand(verboseHelp: verboseHelp));
|
||||
|
@ -0,0 +1,308 @@
|
||||
// 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:meta/meta.dart';
|
||||
|
||||
import '../base/common.dart';
|
||||
import '../base/file_system.dart';
|
||||
import '../base/logger.dart';
|
||||
import '../base/process.dart';
|
||||
import '../base/utils.dart';
|
||||
import '../build_info.dart';
|
||||
import '../build_system/build_system.dart';
|
||||
import '../build_system/targets/macos.dart';
|
||||
import '../cache.dart';
|
||||
import '../flutter_plugins.dart';
|
||||
import '../globals.dart' as globals;
|
||||
import '../macos/cocoapod_utils.dart';
|
||||
import '../runner/flutter_command.dart' show DevelopmentArtifact, FlutterCommandResult;
|
||||
import '../version.dart';
|
||||
import 'build_ios_framework.dart';
|
||||
|
||||
/// Produces a .framework for integration into a host macOS 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 BuildMacOSFrameworkCommand extends BuildFrameworkCommand {
|
||||
BuildMacOSFrameworkCommand({
|
||||
super.flutterVersion,
|
||||
required super.buildSystem,
|
||||
required super.verboseHelp,
|
||||
super.cache,
|
||||
super.platform,
|
||||
});
|
||||
|
||||
@override
|
||||
final String name = 'macos-framework';
|
||||
|
||||
@override
|
||||
final String description = 'Produces .xcframeworks for a Flutter project '
|
||||
'and its plugins for integration into existing, plain macOS Xcode projects.\n'
|
||||
'This can only be run on macOS hosts.';
|
||||
|
||||
@override
|
||||
Future<Set<DevelopmentArtifact>> get requiredArtifacts async => const <DevelopmentArtifact>{
|
||||
DevelopmentArtifact.macOS,
|
||||
};
|
||||
|
||||
@override
|
||||
Future<FlutterCommandResult> runCommand() async {
|
||||
final String outputArgument = stringArg('output') ??
|
||||
globals.fs.path.join(
|
||||
globals.fs.currentDirectory.path,
|
||||
'build',
|
||||
'macos',
|
||||
'framework',
|
||||
);
|
||||
|
||||
if (outputArgument.isEmpty) {
|
||||
throwToolExit('--output is required.');
|
||||
}
|
||||
|
||||
if (!project.macos.existsSync()) {
|
||||
throwToolExit('Project does not support macOS');
|
||||
}
|
||||
|
||||
final Directory outputDirectory =
|
||||
globals.fs.directory(globals.fs.path.absolute(globals.fs.path.normalize(outputArgument)));
|
||||
|
||||
final List<BuildInfo> buildInfos = await getBuildInfos();
|
||||
displayNullSafetyMode(buildInfos.first);
|
||||
|
||||
for (final BuildInfo buildInfo in buildInfos) {
|
||||
globals.printStatus('Building macOS frameworks in ${getNameForBuildMode(buildInfo.mode)} mode...');
|
||||
final String xcodeBuildConfiguration = sentenceCase(getNameForBuildMode(buildInfo.mode));
|
||||
final Directory modeDirectory = outputDirectory.childDirectory(xcodeBuildConfiguration);
|
||||
|
||||
if (modeDirectory.existsSync()) {
|
||||
modeDirectory.deleteSync(recursive: true);
|
||||
}
|
||||
|
||||
if (boolArg('cocoapods') ?? false) {
|
||||
produceFlutterPodspec(buildInfo.mode, modeDirectory, force: boolArg('force') ?? false);
|
||||
}
|
||||
|
||||
// Build aot, create App.framework and copy FlutterMacOS.framework. Make XCFrameworks.
|
||||
await _produceAppFramework(buildInfo, modeDirectory);
|
||||
|
||||
// Build and copy plugins.
|
||||
final Directory buildOutput = modeDirectory.childDirectory('macos');
|
||||
await processPodsIfNeeded(project.macos, getMacOSBuildDirectory(), buildInfo.mode);
|
||||
if (hasPlugins(project)) {
|
||||
await _producePlugins(xcodeBuildConfiguration, buildOutput, modeDirectory);
|
||||
}
|
||||
|
||||
globals.logger.printStatus(' └─Moving to ${globals.fs.path.relative(modeDirectory.path)}');
|
||||
|
||||
// Delete the intermediaries since they would have been copied into our
|
||||
// output frameworks.
|
||||
if (buildOutput.existsSync()) {
|
||||
buildOutput.deleteSync(recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
globals.printStatus('Frameworks written to ${outputDirectory.path}.');
|
||||
|
||||
if (hasPlugins(project)) {
|
||||
// Apps do not generate a FlutterPluginRegistrant.framework. Users will need
|
||||
// to copy GeneratedPluginRegistrant.swift to their project manually.
|
||||
final File pluginRegistrantImplementation = project.macos.pluginRegistrantImplementation;
|
||||
pluginRegistrantImplementation.copySync(outputDirectory.childFile(pluginRegistrantImplementation.basename).path);
|
||||
globals.printStatus('\nCopy ${globals.fs.path.basename(pluginRegistrantImplementation.path)} into your project.');
|
||||
}
|
||||
|
||||
return FlutterCommandResult.success();
|
||||
}
|
||||
|
||||
/// Create podspec that will download and unzip remote engine assets so host apps can leverage CocoaPods
|
||||
/// vendored framework caching.
|
||||
@visibleForTesting
|
||||
void produceFlutterPodspec(BuildMode mode, Directory modeDirectory, {bool force = false}) {
|
||||
final Status status = globals.logger.startProgress(' ├─Creating FlutterMacOS.podspec...');
|
||||
try {
|
||||
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}');
|
||||
}
|
||||
|
||||
// Podspecs use semantic versioning, which don't support hotfixes.
|
||||
// Fake out a semantic version with major.minor.(patch * 100) + hotfix.
|
||||
// A real increasing version is required to prompt CocoaPods to fetch
|
||||
// new artifacts when the source URL changes.
|
||||
final int minorHotfixVersion = (gitTagVersion.z ?? 0) * 100 + (gitTagVersion.hotfix ?? 0);
|
||||
|
||||
final File license = cache.getLicenseFile();
|
||||
if (!license.existsSync()) {
|
||||
throwToolExit('Could not find license at ${license.path}');
|
||||
}
|
||||
final String licenseSource = license.readAsStringSync();
|
||||
final String artifactsMode = mode == BuildMode.debug ? 'darwin-x64' : 'darwin-x64-${mode.name}';
|
||||
|
||||
final String podspecContents = '''
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'FlutterMacOS'
|
||||
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.
|
||||
This pod vends the macOS Flutter engine framework. It is compatible with application frameworks created with this version of the engine and tools.
|
||||
The pod version matches Flutter version major.minor.(patch * 100) + hotfix.
|
||||
DESC
|
||||
s.homepage = 'https://flutter.dev'
|
||||
s.license = { :type => 'BSD', :text => <<-LICENSE
|
||||
$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.documentation_url = 'https://flutter.dev/docs'
|
||||
s.osx.deployment_target = '10.11'
|
||||
s.vendored_frameworks = 'FlutterMacOS.framework'
|
||||
s.prepare_command = 'unzip FlutterMacOS.framework -d FlutterMacOS.framework'
|
||||
end
|
||||
''';
|
||||
|
||||
final File podspec = modeDirectory.childFile('FlutterMacOS.podspec')..createSync(recursive: true);
|
||||
podspec.writeAsStringSync(podspecContents);
|
||||
} finally {
|
||||
status.stop();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _produceAppFramework(
|
||||
BuildInfo buildInfo,
|
||||
Directory outputBuildDirectory,
|
||||
) async {
|
||||
final Status status = globals.logger.startProgress(
|
||||
' ├─Building App.xcframework and FlutterMacOS.xcframework...',
|
||||
);
|
||||
|
||||
try {
|
||||
final Environment environment = Environment(
|
||||
projectDir: globals.fs.currentDirectory,
|
||||
outputDir: outputBuildDirectory,
|
||||
buildDir: project.dartTool.childDirectory('flutter_build'),
|
||||
cacheDir: globals.cache.getRoot(),
|
||||
flutterRootDir: globals.fs.directory(Cache.flutterRoot),
|
||||
defines: <String, String>{
|
||||
kTargetFile: targetFile,
|
||||
kTargetPlatform: getNameForTargetPlatform(TargetPlatform.darwin),
|
||||
kDarwinArchs: <DarwinArch>[
|
||||
DarwinArch.x86_64,
|
||||
DarwinArch.arm64,
|
||||
].map(getNameForDarwinArch).join(' '),
|
||||
...buildInfo.toBuildSystemEnvironment(),
|
||||
},
|
||||
artifacts: globals.artifacts!,
|
||||
fileSystem: globals.fs,
|
||||
logger: globals.logger,
|
||||
processManager: globals.processManager,
|
||||
platform: globals.platform,
|
||||
engineVersion: globals.artifacts!.isLocalEngine ? null : globals.flutterVersion.engineRevision,
|
||||
generateDartPluginRegistry: true,
|
||||
);
|
||||
Target target;
|
||||
// Always build debug for simulator.
|
||||
if (buildInfo.isDebug) {
|
||||
target = const DebugMacOSBundleFlutterAssets();
|
||||
} else if (buildInfo.isProfile) {
|
||||
target = const ProfileMacOSBundleFlutterAssets();
|
||||
} else {
|
||||
target = const ReleaseMacOSBundleFlutterAssets();
|
||||
}
|
||||
|
||||
final BuildResult result = await buildSystem.build(target, environment);
|
||||
if (!result.success) {
|
||||
for (final ExceptionMeasurement measurement in result.exceptions.values) {
|
||||
globals.printError(measurement.exception.toString());
|
||||
}
|
||||
throwToolExit('The App.xcframework build failed.');
|
||||
}
|
||||
} finally {
|
||||
status.stop();
|
||||
}
|
||||
|
||||
final Directory appFramework = outputBuildDirectory.childDirectory('App.framework');
|
||||
await BuildFrameworkCommand.produceXCFramework(
|
||||
<Directory>[appFramework],
|
||||
'App',
|
||||
outputBuildDirectory,
|
||||
globals.processManager,
|
||||
);
|
||||
appFramework.deleteSync(recursive: true);
|
||||
final Directory flutterFramework = outputBuildDirectory.childDirectory('FlutterMacOS.framework');
|
||||
|
||||
// If FlutterMacOS.podspec was generated, do not generate XCFramework.
|
||||
if (!(boolArg('cocoapods') ?? false)) {
|
||||
await BuildFrameworkCommand.produceXCFramework(
|
||||
<Directory>[flutterFramework],
|
||||
'FlutterMacOS',
|
||||
outputBuildDirectory,
|
||||
globals.processManager,
|
||||
);
|
||||
}
|
||||
flutterFramework.deleteSync(recursive: true);
|
||||
}
|
||||
|
||||
Future<void> _producePlugins(
|
||||
String xcodeBuildConfiguration,
|
||||
Directory buildOutput,
|
||||
Directory modeDirectory,
|
||||
) async {
|
||||
final Status status = globals.logger.startProgress(' ├─Building plugins...');
|
||||
try {
|
||||
final List<String> pluginsBuildCommand = <String>[
|
||||
...globals.xcode!.xcrunCommand(),
|
||||
'xcodebuild',
|
||||
'-alltargets',
|
||||
'-sdk',
|
||||
'macosx',
|
||||
'-configuration',
|
||||
xcodeBuildConfiguration,
|
||||
'SYMROOT=${buildOutput.path}',
|
||||
'ONLY_ACTIVE_ARCH=NO', // No device targeted, so build all valid architectures.
|
||||
'BUILD_LIBRARY_FOR_DISTRIBUTION=YES',
|
||||
if (boolArg('static') ?? false) 'MACH_O_TYPE=staticlib',
|
||||
];
|
||||
|
||||
final RunResult buildPluginsResult = await globals.processUtils.run(
|
||||
pluginsBuildCommand,
|
||||
workingDirectory: project.macos.hostAppRoot.childDirectory('Pods').path,
|
||||
);
|
||||
|
||||
if (buildPluginsResult.exitCode != 0) {
|
||||
throwToolExit('Unable to build plugin frameworks: ${buildPluginsResult.stderr}');
|
||||
}
|
||||
|
||||
final Directory buildConfiguration = buildOutput.childDirectory(xcodeBuildConfiguration);
|
||||
|
||||
final Iterable<Directory> products = buildConfiguration.listSync(followLinks: false).whereType<Directory>();
|
||||
for (final Directory builtProduct in products) {
|
||||
for (final FileSystemEntity podProduct in builtProduct.listSync(followLinks: false)) {
|
||||
final String podFrameworkName = podProduct.basename;
|
||||
if (globals.fs.path.extension(podFrameworkName) != '.framework') {
|
||||
continue;
|
||||
}
|
||||
final String binaryName = globals.fs.path.basenameWithoutExtension(podFrameworkName);
|
||||
|
||||
await BuildFrameworkCommand.produceXCFramework(
|
||||
<Directory>[
|
||||
podProduct as Directory,
|
||||
],
|
||||
binaryName,
|
||||
modeDirectory,
|
||||
globals.processManager,
|
||||
);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
status.stop();
|
||||
}
|
||||
}
|
||||
}
|
@ -518,6 +518,8 @@ class MacOSProject extends XcodeBasedProject {
|
||||
@override
|
||||
File get generatedXcodePropertiesFile => ephemeralDirectory.childFile('Flutter-Generated.xcconfig');
|
||||
|
||||
File get pluginRegistrantImplementation => managedDirectory.childFile('GeneratedPluginRegistrant.swift');
|
||||
|
||||
@override
|
||||
File xcodeConfigFor(String mode) => managedDirectory.childFile('Flutter-$mode.xcconfig');
|
||||
|
||||
|
@ -11,6 +11,7 @@ import 'package:flutter_tools/src/build_info.dart';
|
||||
import 'package:flutter_tools/src/build_system/build_system.dart';
|
||||
import 'package:flutter_tools/src/cache.dart';
|
||||
import 'package:flutter_tools/src/commands/build_ios_framework.dart';
|
||||
import 'package:flutter_tools/src/commands/build_macos_framework.dart';
|
||||
import 'package:flutter_tools/src/version.dart';
|
||||
|
||||
import '../../src/common.dart';
|
||||
@ -19,31 +20,31 @@ import '../../src/fakes.dart';
|
||||
import '../../src/test_build_system.dart';
|
||||
|
||||
void main() {
|
||||
MemoryFileSystem memoryFileSystem;
|
||||
Directory outputDirectory;
|
||||
FakePlatform fakePlatform;
|
||||
|
||||
setUpAll(() {
|
||||
Cache.disableLocking();
|
||||
});
|
||||
|
||||
const String storageBaseUrl = 'https://fake.googleapis.com';
|
||||
setUp(() {
|
||||
memoryFileSystem = MemoryFileSystem.test();
|
||||
fakePlatform = FakePlatform(
|
||||
operatingSystem: 'macos',
|
||||
environment: <String, String>{
|
||||
'FLUTTER_STORAGE_BASE_URL': storageBaseUrl,
|
||||
},
|
||||
);
|
||||
|
||||
outputDirectory = memoryFileSystem.systemTempDirectory
|
||||
.createTempSync('flutter_build_framework_test_output.')
|
||||
.childDirectory('Debug')
|
||||
..createSync();
|
||||
});
|
||||
|
||||
group('build ios-framework', () {
|
||||
MemoryFileSystem memoryFileSystem;
|
||||
Directory outputDirectory;
|
||||
FakePlatform fakePlatform;
|
||||
|
||||
setUpAll(() {
|
||||
Cache.disableLocking();
|
||||
});
|
||||
|
||||
const String storageBaseUrl = 'https://fake.googleapis.com';
|
||||
setUp(() {
|
||||
memoryFileSystem = MemoryFileSystem.test();
|
||||
fakePlatform = FakePlatform(
|
||||
operatingSystem: 'macos',
|
||||
environment: <String, String>{
|
||||
'FLUTTER_STORAGE_BASE_URL': storageBaseUrl,
|
||||
},
|
||||
);
|
||||
|
||||
outputDirectory = memoryFileSystem.systemTempDirectory
|
||||
.createTempSync('flutter_build_ios_framework_test_output.')
|
||||
.childDirectory('Debug')
|
||||
..createSync();
|
||||
});
|
||||
|
||||
group('podspec', () {
|
||||
const String engineRevision = '0123456789abcdef';
|
||||
Cache cache;
|
||||
@ -272,6 +273,235 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
group('build macos-framework', () {
|
||||
group('podspec', () {
|
||||
const String engineRevision = '0123456789abcdef';
|
||||
Cache cache;
|
||||
|
||||
setUp(() {
|
||||
final Directory rootOverride = memoryFileSystem.directory('cache');
|
||||
cache = Cache.test(
|
||||
rootOverride: rootOverride,
|
||||
platform: fakePlatform,
|
||||
fileSystem: memoryFileSystem,
|
||||
processManager: FakeProcessManager.any(),
|
||||
);
|
||||
rootOverride.childDirectory('bin').childDirectory('internal').childFile('engine.version')
|
||||
..createSync(recursive: true)
|
||||
..writeAsStringSync(engineRevision);
|
||||
});
|
||||
|
||||
testUsingContext('version unknown', () async {
|
||||
const String frameworkVersion = '0.0.0-unknown';
|
||||
final FakeFlutterVersion fakeFlutterVersion = FakeFlutterVersion(frameworkVersion: frameworkVersion);
|
||||
|
||||
final BuildMacOSFrameworkCommand command = BuildMacOSFrameworkCommand(
|
||||
buildSystem: TestBuildSystem.all(BuildResult(success: true)),
|
||||
platform: fakePlatform,
|
||||
flutterVersion: fakeFlutterVersion,
|
||||
cache: cache,
|
||||
verboseHelp: false,
|
||||
);
|
||||
|
||||
expect(() => command.produceFlutterPodspec(BuildMode.debug, outputDirectory),
|
||||
throwsToolExit(message: 'Detected version is $frameworkVersion'));
|
||||
}, overrides: <Type, Generator>{
|
||||
FileSystem: () => memoryFileSystem,
|
||||
ProcessManager: () => FakeProcessManager.any(),
|
||||
});
|
||||
|
||||
testUsingContext('throws when not on a released version', () async {
|
||||
const String frameworkVersion = 'v1.13.10+hotfix-pre.2';
|
||||
const GitTagVersion gitTagVersion = GitTagVersion(
|
||||
x: 1,
|
||||
y: 13,
|
||||
z: 10,
|
||||
hotfix: 13,
|
||||
commits: 2,
|
||||
);
|
||||
final FakeFlutterVersion fakeFlutterVersion = FakeFlutterVersion(
|
||||
gitTagVersion: gitTagVersion,
|
||||
frameworkVersion: frameworkVersion,
|
||||
);
|
||||
|
||||
final BuildMacOSFrameworkCommand command = BuildMacOSFrameworkCommand(
|
||||
buildSystem: TestBuildSystem.all(BuildResult(success: true)),
|
||||
platform: fakePlatform,
|
||||
flutterVersion: fakeFlutterVersion,
|
||||
cache: cache,
|
||||
verboseHelp: false,
|
||||
);
|
||||
|
||||
expect(() => command.produceFlutterPodspec(BuildMode.debug, outputDirectory),
|
||||
throwsToolExit(message: 'Detected version is $frameworkVersion'));
|
||||
}, overrides: <Type, Generator>{
|
||||
FileSystem: () => memoryFileSystem,
|
||||
ProcessManager: () => FakeProcessManager.any(),
|
||||
});
|
||||
|
||||
testUsingContext('throws when license not found', () async {
|
||||
final FakeFlutterVersion fakeFlutterVersion = FakeFlutterVersion(
|
||||
gitTagVersion: const GitTagVersion(
|
||||
x: 1,
|
||||
y: 13,
|
||||
z: 10,
|
||||
hotfix: 13,
|
||||
commits: 0,
|
||||
),
|
||||
);
|
||||
|
||||
final BuildMacOSFrameworkCommand command = BuildMacOSFrameworkCommand(
|
||||
buildSystem: TestBuildSystem.all(BuildResult(success: true)),
|
||||
platform: fakePlatform,
|
||||
flutterVersion: fakeFlutterVersion,
|
||||
cache: cache,
|
||||
verboseHelp: false,
|
||||
);
|
||||
|
||||
expect(() => command.produceFlutterPodspec(BuildMode.debug, outputDirectory),
|
||||
throwsToolExit(message: 'Could not find license'));
|
||||
}, overrides: <Type, Generator>{
|
||||
FileSystem: () => memoryFileSystem,
|
||||
ProcessManager: () => FakeProcessManager.any(),
|
||||
});
|
||||
|
||||
group('is created', () {
|
||||
const String frameworkVersion = 'v1.13.11+hotfix.13';
|
||||
const String licenseText = 'This is the license!';
|
||||
|
||||
setUp(() {
|
||||
// cache.getLicenseFile() relies on the flutter root being set.
|
||||
Cache.flutterRoot ??= getFlutterRoot();
|
||||
cache.getLicenseFile()
|
||||
..createSync(recursive: true)
|
||||
..writeAsStringSync(licenseText);
|
||||
});
|
||||
|
||||
group('on master channel', () {
|
||||
testUsingContext('created when forced', () async {
|
||||
const GitTagVersion gitTagVersion = GitTagVersion(
|
||||
x: 1,
|
||||
y: 13,
|
||||
z: 11,
|
||||
hotfix: 13,
|
||||
commits: 100,
|
||||
);
|
||||
final FakeFlutterVersion fakeFlutterVersion = FakeFlutterVersion(
|
||||
gitTagVersion: gitTagVersion,
|
||||
frameworkVersion: frameworkVersion,
|
||||
);
|
||||
|
||||
final BuildMacOSFrameworkCommand command = BuildMacOSFrameworkCommand(
|
||||
buildSystem: TestBuildSystem.all(BuildResult(success: true)),
|
||||
platform: fakePlatform,
|
||||
flutterVersion: fakeFlutterVersion,
|
||||
cache: cache,
|
||||
verboseHelp: false,
|
||||
);
|
||||
command.produceFlutterPodspec(BuildMode.debug, outputDirectory, force: true);
|
||||
|
||||
final File expectedPodspec = outputDirectory.childFile('FlutterMacOS.podspec');
|
||||
expect(expectedPodspec.existsSync(), isTrue);
|
||||
}, overrides: <Type, Generator>{
|
||||
FileSystem: () => memoryFileSystem,
|
||||
ProcessManager: () => FakeProcessManager.any(),
|
||||
});
|
||||
});
|
||||
|
||||
group('not on master channel', () {
|
||||
FakeFlutterVersion fakeFlutterVersion;
|
||||
setUp(() {
|
||||
const GitTagVersion gitTagVersion = GitTagVersion(
|
||||
x: 1,
|
||||
y: 13,
|
||||
z: 11,
|
||||
hotfix: 13,
|
||||
commits: 0,
|
||||
);
|
||||
fakeFlutterVersion = FakeFlutterVersion(
|
||||
gitTagVersion: gitTagVersion,
|
||||
frameworkVersion: frameworkVersion,
|
||||
);
|
||||
});
|
||||
|
||||
testUsingContext('contains license and version', () async {
|
||||
final BuildMacOSFrameworkCommand command = BuildMacOSFrameworkCommand(
|
||||
buildSystem: TestBuildSystem.all(BuildResult(success: true)),
|
||||
platform: fakePlatform,
|
||||
flutterVersion: fakeFlutterVersion,
|
||||
cache: cache,
|
||||
verboseHelp: false,
|
||||
);
|
||||
command.produceFlutterPodspec(BuildMode.debug, outputDirectory);
|
||||
|
||||
final File expectedPodspec = outputDirectory.childFile('FlutterMacOS.podspec');
|
||||
final String podspecContents = expectedPodspec.readAsStringSync();
|
||||
expect(podspecContents, contains("'1.13.1113'"));
|
||||
expect(podspecContents, contains('# $frameworkVersion'));
|
||||
expect(podspecContents, contains(licenseText));
|
||||
}, overrides: <Type, Generator>{
|
||||
FileSystem: () => memoryFileSystem,
|
||||
ProcessManager: () => FakeProcessManager.any(),
|
||||
});
|
||||
|
||||
testUsingContext('debug URL', () async {
|
||||
final BuildMacOSFrameworkCommand command = BuildMacOSFrameworkCommand(
|
||||
buildSystem: TestBuildSystem.all(BuildResult(success: true)),
|
||||
platform: fakePlatform,
|
||||
flutterVersion: fakeFlutterVersion,
|
||||
cache: cache,
|
||||
verboseHelp: false,
|
||||
);
|
||||
command.produceFlutterPodspec(BuildMode.debug, outputDirectory);
|
||||
|
||||
final File expectedPodspec = outputDirectory.childFile('FlutterMacOS.podspec');
|
||||
final String podspecContents = expectedPodspec.readAsStringSync();
|
||||
expect(podspecContents, contains("'$storageBaseUrl/flutter_infra_release/flutter/$engineRevision/darwin-x64/artifacts.zip'"));
|
||||
}, overrides: <Type, Generator>{
|
||||
FileSystem: () => memoryFileSystem,
|
||||
ProcessManager: () => FakeProcessManager.any(),
|
||||
});
|
||||
|
||||
testUsingContext('profile URL', () async {
|
||||
final BuildMacOSFrameworkCommand command = BuildMacOSFrameworkCommand(
|
||||
buildSystem: TestBuildSystem.all(BuildResult(success: true)),
|
||||
platform: fakePlatform,
|
||||
flutterVersion: fakeFlutterVersion,
|
||||
cache: cache,
|
||||
verboseHelp: false,
|
||||
);
|
||||
command.produceFlutterPodspec(BuildMode.profile, outputDirectory);
|
||||
|
||||
final File expectedPodspec = outputDirectory.childFile('FlutterMacOS.podspec');
|
||||
final String podspecContents = expectedPodspec.readAsStringSync();
|
||||
expect(podspecContents, contains("'$storageBaseUrl/flutter_infra_release/flutter/$engineRevision/darwin-x64-profile/artifacts.zip'"));
|
||||
}, overrides: <Type, Generator>{
|
||||
FileSystem: () => memoryFileSystem,
|
||||
ProcessManager: () => FakeProcessManager.any(),
|
||||
});
|
||||
|
||||
testUsingContext('release URL', () async {
|
||||
final BuildMacOSFrameworkCommand command = BuildMacOSFrameworkCommand(
|
||||
buildSystem: TestBuildSystem.all(BuildResult(success: true)),
|
||||
platform: fakePlatform,
|
||||
flutterVersion: fakeFlutterVersion,
|
||||
cache: cache,
|
||||
verboseHelp: false,
|
||||
);
|
||||
command.produceFlutterPodspec(BuildMode.release, outputDirectory);
|
||||
|
||||
final File expectedPodspec = outputDirectory.childFile('FlutterMacOS.podspec');
|
||||
final String podspecContents = expectedPodspec.readAsStringSync();
|
||||
expect(podspecContents, contains("'$storageBaseUrl/flutter_infra_release/flutter/$engineRevision/darwin-x64-release/artifacts.zip'"));
|
||||
}, overrides: <Type, Generator>{
|
||||
FileSystem: () => memoryFileSystem,
|
||||
ProcessManager: () => FakeProcessManager.any(),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
group('XCFrameworks', () {
|
||||
MemoryFileSystem fileSystem;
|
||||
FakeProcessManager fakeProcessManager;
|
||||
|
@ -251,7 +251,7 @@ void main() {
|
||||
final FlutterProject project = await someProject();
|
||||
project.macos.managedDirectory.createSync(recursive: true);
|
||||
await project.regeneratePlatformSpecificTooling();
|
||||
expectExists(project.macos.managedDirectory.childFile('GeneratedPluginRegistrant.swift'));
|
||||
expectExists(project.macos.pluginRegistrantImplementation);
|
||||
}, overrides: <Type, Generator>{
|
||||
FileSystem: () => MemoryFileSystem.test(),
|
||||
ProcessManager: () => FakeProcessManager.any(),
|
||||
|
Loading…
x
Reference in New Issue
Block a user