Skip to content

Commit 3f1f0a8

Browse files
authored
Add flutter build macos-framework command (#105242)
1 parent 975e04b commit 3f1f0a8

File tree

7 files changed

+881
-42
lines changed

7 files changed

+881
-42
lines changed

dev/devicelab/bin/tasks/build_ios_framework_module_test.dart

Lines changed: 306 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import 'package:flutter_devicelab/framework/task_result.dart';
1010
import 'package:flutter_devicelab/framework/utils.dart';
1111
import 'package:path/path.dart' as path;
1212

13-
/// Tests that iOS .xcframeworks can be built.
13+
/// Tests that iOS and macOS .xcframeworks can be built.
1414
Future<void> main() async {
1515
await task(() async {
1616

@@ -19,7 +19,7 @@ Future<void> main() async {
1919
final Directory tempDir = Directory.systemTemp.createTempSync('flutter_module_test.');
2020
try {
2121
await inDirectory(tempDir, () async {
22-
section('Test module template');
22+
section('Test iOS module template');
2323

2424
final Directory moduleProjectDir =
2525
Directory(path.join(tempDir.path, 'hello_module'));
@@ -34,6 +34,7 @@ Future<void> main() async {
3434
],
3535
);
3636

37+
await _addPlugin(moduleProjectDir);
3738
await _testBuildIosFramework(moduleProjectDir, isModule: true);
3839

3940
section('Test app template');
@@ -45,7 +46,9 @@ Future<void> main() async {
4546
options: <String>['--org', 'io.flutter.devicelab', 'hello_project'],
4647
);
4748

49+
await _addPlugin(projectDir);
4850
await _testBuildIosFramework(projectDir);
51+
await _testBuildMacOSFramework(projectDir);
4952
});
5053

5154
return TaskResult.success(null);
@@ -59,7 +62,7 @@ Future<void> main() async {
5962
});
6063
}
6164

62-
Future<void> _testBuildIosFramework(Directory projectDir, { bool isModule = false}) async {
65+
Future<void> _addPlugin(Directory projectDir) async {
6366
section('Add plugins');
6467

6568
final File pubspec = File(path.join(projectDir.path, 'pubspec.yaml'));
@@ -75,24 +78,11 @@ Future<void> _testBuildIosFramework(Directory projectDir, { bool isModule = fals
7578
options: <String>['get'],
7679
);
7780
});
81+
}
7882

79-
// First, build the module in Debug to copy the debug version of Flutter.xcframework.
80-
// This proves "flutter build ios-framework" re-copies the relevant Flutter.xcframework,
81-
// otherwise building plugins with bitcode will fail linking because the debug version
82-
// of Flutter.xcframework does not contain bitcode.
83-
await inDirectory(projectDir, () async {
84-
await flutter(
85-
'build',
86-
options: <String>[
87-
'ios',
88-
'--debug',
89-
'--no-codesign',
90-
],
91-
);
92-
});
93-
83+
Future<void> _testBuildIosFramework(Directory projectDir, { bool isModule = false}) async {
9484
// This builds all build modes' frameworks by default
95-
section('Build frameworks');
85+
section('Build iOS app');
9686

9787
const String outputDirectoryName = 'flutter-frameworks';
9888

@@ -488,6 +478,293 @@ Future<void> _testBuildIosFramework(Directory projectDir, { bool isModule = fals
488478
}
489479
}
490480

481+
482+
Future<void> _testBuildMacOSFramework(Directory projectDir) async {
483+
// This builds all build modes' frameworks by default
484+
section('Build macOS frameworks');
485+
486+
const String outputDirectoryName = 'flutter-frameworks';
487+
488+
await inDirectory(projectDir, () async {
489+
await flutter(
490+
'build',
491+
options: <String>[
492+
'macos-framework',
493+
'--verbose',
494+
'--output=$outputDirectoryName',
495+
'--obfuscate',
496+
'--split-debug-info=symbols',
497+
],
498+
);
499+
});
500+
501+
final String outputPath = path.join(projectDir.path, outputDirectoryName);
502+
final String flutterFramework = path.join(
503+
outputPath,
504+
'Debug',
505+
'FlutterMacOS.xcframework',
506+
'macos-arm64_x86_64',
507+
'FlutterMacOS.framework',
508+
);
509+
checkDirectoryExists(flutterFramework);
510+
511+
final String debugAppFrameworkPath = path.join(
512+
outputPath,
513+
'Debug',
514+
'App.xcframework',
515+
'macos-arm64_x86_64',
516+
'App.framework',
517+
'App',
518+
);
519+
checkSymlinkExists(debugAppFrameworkPath);
520+
521+
checkFileExists(path.join(
522+
outputPath,
523+
'Debug',
524+
'App.xcframework',
525+
'macos-arm64_x86_64',
526+
'App.framework',
527+
'Resources',
528+
'Info.plist',
529+
));
530+
531+
section('Check debug build has Dart snapshot as asset');
532+
533+
checkFileExists(path.join(
534+
outputPath,
535+
'Debug',
536+
'App.xcframework',
537+
'macos-arm64_x86_64',
538+
'App.framework',
539+
'Resources',
540+
'flutter_assets',
541+
'vm_snapshot_data',
542+
));
543+
544+
section('Check obfuscation symbols');
545+
546+
checkFileExists(path.join(
547+
projectDir.path,
548+
'symbols',
549+
'app.darwin-arm64.symbols',
550+
));
551+
552+
checkFileExists(path.join(
553+
projectDir.path,
554+
'symbols',
555+
'app.darwin-x86_64.symbols',
556+
));
557+
558+
section('Check debug build has no Dart AOT');
559+
560+
final String aotSymbols = await _dylibSymbols(debugAppFrameworkPath);
561+
562+
if (aotSymbols.contains('architecture') ||
563+
aotSymbols.contains('_kDartVmSnapshot')) {
564+
throw TaskResult.failure('Debug App.framework contains AOT');
565+
}
566+
567+
section('Check profile, release builds has Dart AOT dylib');
568+
569+
for (final String mode in <String>['Profile', 'Release']) {
570+
final String appFrameworkPath = path.join(
571+
outputPath,
572+
mode,
573+
'App.xcframework',
574+
'macos-arm64_x86_64',
575+
'App.framework',
576+
'App',
577+
);
578+
579+
await _checkDylib(appFrameworkPath);
580+
581+
final String aotSymbols = await _dylibSymbols(appFrameworkPath);
582+
583+
if (!aotSymbols.contains('_kDartVmSnapshot')) {
584+
throw TaskResult.failure('$mode App.framework missing Dart AOT');
585+
}
586+
587+
checkFileNotExists(path.join(
588+
outputPath,
589+
mode,
590+
'App.xcframework',
591+
'macos-arm64_x86_64',
592+
'App.framework',
593+
'Resources',
594+
'flutter_assets',
595+
'vm_snapshot_data',
596+
));
597+
598+
checkFileExists(path.join(
599+
outputPath,
600+
mode,
601+
'App.xcframework',
602+
'macos-arm64_x86_64',
603+
'App.framework',
604+
'Resources',
605+
'Info.plist',
606+
));
607+
}
608+
609+
section("Check all modes' engine dylib");
610+
611+
for (final String mode in <String>['Debug', 'Profile', 'Release']) {
612+
final String engineBinary = path.join(
613+
outputPath,
614+
mode,
615+
'FlutterMacOS.xcframework',
616+
'macos-arm64_x86_64',
617+
'FlutterMacOS.framework',
618+
'FlutterMacOS',
619+
);
620+
checkSymlinkExists(engineBinary);
621+
622+
checkFileExists(path.join(
623+
outputPath,
624+
mode,
625+
'FlutterMacOS.xcframework',
626+
'macos-arm64_x86_64',
627+
'FlutterMacOS.framework',
628+
'Headers',
629+
'FlutterMacOS.h',
630+
));
631+
}
632+
633+
section('Check all modes have plugins');
634+
635+
for (final String mode in <String>['Debug', 'Profile', 'Release']) {
636+
final String pluginFrameworkPath = path.join(
637+
outputPath,
638+
mode,
639+
'connectivity_macos.xcframework',
640+
'macos-arm64_x86_64',
641+
'connectivity_macos.framework',
642+
'connectivity_macos',
643+
);
644+
645+
await _checkDylib(pluginFrameworkPath);
646+
if (!await _linksOnFlutterMacOS(pluginFrameworkPath)) {
647+
throw TaskResult.failure('$pluginFrameworkPath does not link on Flutter');
648+
}
649+
650+
final String transitiveDependencyFrameworkPath = path.join(
651+
outputPath,
652+
mode,
653+
'Reachability.xcframework',
654+
'macos-arm64_x86_64',
655+
'Reachability.framework',
656+
'Reachability',
657+
);
658+
if (await _linksOnFlutterMacOS(transitiveDependencyFrameworkPath)) {
659+
throw TaskResult.failure('Transitive dependency $transitiveDependencyFrameworkPath unexpectedly links on Flutter');
660+
}
661+
662+
checkFileExists(path.join(
663+
outputPath,
664+
mode,
665+
'connectivity_macos.xcframework',
666+
'macos-arm64_x86_64',
667+
'connectivity_macos.framework',
668+
'Headers',
669+
'connectivity_macos-Swift.h',
670+
));
671+
672+
checkDirectoryExists(path.join(
673+
outputPath,
674+
mode,
675+
'connectivity_macos.xcframework',
676+
'macos-arm64_x86_64',
677+
'connectivity_macos.framework',
678+
'Modules',
679+
'connectivity_macos.swiftmodule',
680+
));
681+
682+
if (mode != 'Debug') {
683+
checkDirectoryExists(path.join(
684+
outputPath,
685+
mode,
686+
'connectivity_macos.xcframework',
687+
'macos-arm64_x86_64',
688+
'dSYMs',
689+
'connectivity_macos.framework.dSYM',
690+
));
691+
}
692+
693+
checkSymlinkExists(path.join(
694+
outputPath,
695+
mode,
696+
'connectivity_macos.xcframework',
697+
'macos-arm64_x86_64',
698+
'connectivity_macos.framework',
699+
'connectivity_macos',
700+
));
701+
}
702+
703+
// This builds all build modes' frameworks by default
704+
section('Build podspec and static plugins');
705+
706+
const String cocoapodsOutputDirectoryName = 'flutter-frameworks-cocoapods';
707+
708+
await inDirectory(projectDir, () async {
709+
await flutter(
710+
'build',
711+
options: <String>[
712+
'macos-framework',
713+
'--cocoapods',
714+
'--force', // Allow podspec creation on master.
715+
'--output=$cocoapodsOutputDirectoryName',
716+
'--static',
717+
],
718+
);
719+
});
720+
721+
final String cocoapodsOutputPath = path.join(projectDir.path, cocoapodsOutputDirectoryName);
722+
for (final String mode in <String>['Debug', 'Profile', 'Release']) {
723+
checkFileExists(path.join(
724+
cocoapodsOutputPath,
725+
mode,
726+
'FlutterMacOS.podspec',
727+
));
728+
await _checkDylib(path.join(
729+
cocoapodsOutputPath,
730+
mode,
731+
'App.xcframework',
732+
'macos-arm64_x86_64',
733+
'App.framework',
734+
'App',
735+
));
736+
737+
await _checkStatic(path.join(
738+
cocoapodsOutputPath,
739+
mode,
740+
'package_info.xcframework',
741+
'macos-arm64_x86_64',
742+
'package_info.framework',
743+
'package_info',
744+
));
745+
746+
await _checkStatic(path.join(
747+
cocoapodsOutputPath,
748+
mode,
749+
'connectivity_macos.xcframework',
750+
'macos-arm64_x86_64',
751+
'connectivity_macos.framework',
752+
'connectivity_macos',
753+
));
754+
755+
checkDirectoryExists(path.join(
756+
cocoapodsOutputPath,
757+
mode,
758+
'Reachability.xcframework',
759+
));
760+
}
761+
762+
checkFileExists(path.join(
763+
outputPath,
764+
'GeneratedPluginRegistrant.swift',
765+
));
766+
}
767+
491768
Future<void> _checkDylib(String pathToLibrary) async {
492769
final String binaryFileType = await fileType(pathToLibrary);
493770
if (!binaryFileType.contains('dynamically linked')) {
@@ -529,3 +806,13 @@ Future<bool> _linksOnFlutter(String pathToBinary) async {
529806
]);
530807
return loadCommands.contains('Flutter.framework');
531808
}
809+
810+
Future<bool> _linksOnFlutterMacOS(String pathToBinary) async {
811+
final String loadCommands = await eval('otool', <String>[
812+
'-l',
813+
'-arch',
814+
'arm64',
815+
pathToBinary,
816+
]);
817+
return loadCommands.contains('FlutterMacOS.framework');
818+
}

dev/devicelab/lib/framework/utils.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -753,6 +753,13 @@ void checkDirectoryNotExists(String directory) {
753753
}
754754
}
755755

756+
/// Checks that the symlink exists, otherwise throws a [FileSystemException].
757+
void checkSymlinkExists(String file) {
758+
if (!exists(Link(file))) {
759+
throw FileSystemException('Expected symlink to exist.', file);
760+
}
761+
}
762+
756763
/// Check that `collection` contains all entries in `values`.
757764
void checkCollectionContains<T>(Iterable<T> values, Iterable<T> collection) {
758765
for (final T value in values) {

0 commit comments

Comments
 (0)