flutter/packages/flutter_tools/test/integration.shard/native_assets_test.dart
Daco Harkes 2e229be2ff
Native assets: package in framework on iOS and MacOS (#140907)
Packages the native assets for iOS and MacOS in frameworks.

Issue:

* https://github.com/flutter/flutter/issues/140544
* https://github.com/flutter/flutter/issues/129757

## Details

* [x] This packages dylibs from the native assets feature in frameworks. It packages every dylib in a separate framework.
* [x] The dylib name is updated to use `@rpath` instead of `@executable_path`.
* [x] The dylibs for flutter-tester are no longer modified to change the install name. (Previously it was wrongly updating the install name to the location the dylib would have once deployed in an app.)
* [x] Use symlinking on MacOS.
2024-01-19 20:29:13 +00:00

501 lines
19 KiB
Dart

// 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.
// This test exercises the embedding of the native assets mapping in dill files.
// An initial dill file is created by `flutter assemble` and used for running
// the application. This dill must contain the mapping.
// When doing hot reload, this mapping must stay in place.
// When doing a hot restart, a new dill file is pushed. This dill file must also
// contain the native assets mapping.
// When doing a hot reload, this mapping must stay in place.
@Timeout(Duration(minutes: 10))
library;
import 'dart:io';
import 'package:file/file.dart';
import 'package:file_testing/file_testing.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/os.dart';
import 'package:native_assets_cli/native_assets_cli.dart';
import '../src/common.dart';
import 'test_utils.dart' show ProcessResultMatcher, fileSystem, platform;
import 'transition_test_utils.dart';
final String hostOs = platform.operatingSystem;
final List<String> devices = <String>[
'flutter-tester',
hostOs,
];
final List<String> buildSubcommands = <String>[
hostOs,
if (hostOs == 'macos') 'ios',
'apk',
];
final List<String> add2appBuildSubcommands = <String>[
if (hostOs == 'macos') ...<String>[
'macos-framework',
'ios-framework',
],
];
/// The build modes to target for each flutter command that supports passing
/// a build mode.
///
/// The flow of compiling kernel as well as bundling dylibs can differ based on
/// build mode, so we should cover this.
const List<String> buildModes = <String>[
'debug',
'profile',
'release',
];
const String packageName = 'package_with_native_assets';
const String exampleAppName = '${packageName}_example';
void main() {
if (!platform.isMacOS && !platform.isLinux && !platform.isWindows) {
// TODO(dacoharkes): Implement Fuchsia. https://github.com/flutter/flutter/issues/129757
return;
}
setUpAll(() {
processManager.runSync(<String>[
flutterBin,
'config',
'--enable-native-assets',
]);
});
for (final String device in devices) {
for (final String buildMode in buildModes) {
if (device == 'flutter-tester' && buildMode != 'debug') {
continue;
}
final String hotReload = buildMode == 'debug' ? ' hot reload and hot restart' : '';
testWithoutContext('flutter run$hotReload with native assets $device $buildMode', () async {
await inTempDir((Directory tempDirectory) async {
final Directory packageDirectory = await createTestProject(packageName, tempDirectory);
final Directory exampleDirectory = packageDirectory.childDirectory('example');
final ProcessTestResult result = await runFlutter(
<String>[
'run',
'-d$device',
'--$buildMode',
],
exampleDirectory.path,
<Transition>[
Multiple(<Pattern>[
'Flutter run key commands.',
], handler: (String line) {
if (buildMode == 'debug') {
// Do a hot reload diff on the initial dill file.
return 'r';
} else {
// No hot reload and hot restart in release mode.
return 'q';
}
}),
if (buildMode == 'debug') ...<Transition>[
Barrier(
'Performing hot reload...'.padRight(progressMessageWidth),
logging: true,
),
Multiple(<Pattern>[
RegExp('Reloaded .*'),
], handler: (String line) {
// Do a hot restart, pushing a new complete dill file.
return 'R';
}),
Barrier('Performing hot restart...'.padRight(progressMessageWidth)),
Multiple(<Pattern>[
RegExp('Restarted application .*'),
], handler: (String line) {
// Do another hot reload, pushing a diff to the second dill file.
return 'r';
}),
Barrier(
'Performing hot reload...'.padRight(progressMessageWidth),
logging: true,
),
Multiple(<Pattern>[
RegExp('Reloaded .*'),
], handler: (String line) {
return 'q';
}),
],
const Barrier('Application finished.'),
],
logging: false,
);
if (result.exitCode != 0) {
throw Exception('flutter run failed: ${result.exitCode}\n${result.stderr}\n${result.stdout}');
}
final String stdout = result.stdout.join('\n');
// Check that we did not fail to resolve the native function in the
// dynamic library.
expect(stdout, isNot(contains("Invalid argument(s): Couldn't resolve native function 'sum'")));
// And also check that we did not have any other exceptions that might
// shadow the exception we would have gotten.
expect(stdout, isNot(contains('EXCEPTION CAUGHT BY WIDGETS LIBRARY')));
if (device == 'macos') {
expectDylibIsBundledMacOS(exampleDirectory, buildMode);
} else if (device == 'linux') {
expectDylibIsBundledLinux(exampleDirectory, buildMode);
} else if (device == 'windows') {
expectDylibIsBundledWindows(exampleDirectory, buildMode);
}
if (device == hostOs) {
expectCCompilerIsConfigured(exampleDirectory);
}
});
});
}
}
testWithoutContext('flutter test with native assets', () async {
await inTempDir((Directory tempDirectory) async {
final Directory packageDirectory = await createTestProject(packageName, tempDirectory);
final ProcessTestResult result = await runFlutter(
<String>[
'test',
],
packageDirectory.path,
<Transition>[
Barrier(RegExp('.* All tests passed!')),
],
logging: false,
);
if (result.exitCode != 0) {
throw Exception('flutter test failed: ${result.exitCode}\n${result.stderr}\n${result.stdout}');
}
});
});
for (final String buildSubcommand in buildSubcommands) {
for (final String buildMode in buildModes) {
testWithoutContext('flutter build $buildSubcommand with native assets $buildMode', () async {
await inTempDir((Directory tempDirectory) async {
final Directory packageDirectory = await createTestProject(packageName, tempDirectory);
final Directory exampleDirectory = packageDirectory.childDirectory('example');
final ProcessResult result = processManager.runSync(
<String>[
flutterBin,
'build',
buildSubcommand,
'--$buildMode',
if (buildSubcommand == 'ios') '--no-codesign',
],
workingDirectory: exampleDirectory.path,
);
if (result.exitCode != 0) {
throw Exception('flutter build failed: ${result.exitCode}\n${result.stderr}\n${result.stdout}');
}
if (buildSubcommand == 'macos') {
expectDylibIsBundledMacOS(exampleDirectory, buildMode);
} else if (buildSubcommand == 'ios') {
expectDylibIsBundledIos(exampleDirectory, buildMode);
} else if (buildSubcommand == 'linux') {
expectDylibIsBundledLinux(exampleDirectory, buildMode);
} else if (buildSubcommand == 'windows') {
expectDylibIsBundledWindows(exampleDirectory, buildMode);
} else if (buildSubcommand == 'apk') {
expectDylibIsBundledAndroid(exampleDirectory, buildMode);
}
expectCCompilerIsConfigured(exampleDirectory);
});
});
}
// This could be an hermetic unit test if the native_assets_builder
// could mock process runs and file system.
// https://github.com/dart-lang/native/issues/90.
testWithoutContext('flutter build $buildSubcommand error on static libraries', () async {
await inTempDir((Directory tempDirectory) async {
final Directory packageDirectory = await createTestProject(packageName, tempDirectory);
final File buildDotDart = packageDirectory.childFile('build.dart');
final String buildDotDartContents = await buildDotDart.readAsString();
// Overrides the build to output static libraries.
final String buildDotDartContentsNew = buildDotDartContents.replaceFirst(
'final buildConfig = await BuildConfig.fromArgs(args);',
r'''
final buildConfig = await BuildConfig.fromArgs([
'-D${LinkModePreference.configKey}=${LinkModePreference.static}',
...args,
]);
''',
);
expect(buildDotDartContentsNew, isNot(buildDotDartContents));
await buildDotDart.writeAsString(buildDotDartContentsNew);
final Directory exampleDirectory = packageDirectory.childDirectory('example');
final ProcessResult result = processManager.runSync(
<String>[
flutterBin,
'build',
buildSubcommand,
if (buildSubcommand == 'ios') '--no-codesign',
if (buildSubcommand == 'windows') '-v' // Requires verbose mode for error.
],
workingDirectory: exampleDirectory.path,
);
expect(
(result.stdout as String) + (result.stderr as String),
contains('link mode set to static, but this is not yet supported'),
);
expect(result.exitCode, isNot(0));
});
});
}
for (final String add2appBuildSubcommand in add2appBuildSubcommands) {
testWithoutContext('flutter build $add2appBuildSubcommand with native assets', () async {
await inTempDir((Directory tempDirectory) async {
final Directory packageDirectory = await createTestProject(packageName, tempDirectory);
final Directory exampleDirectory = packageDirectory.childDirectory('example');
final ProcessResult result = processManager.runSync(
<String>[
flutterBin,
'build',
add2appBuildSubcommand,
],
workingDirectory: exampleDirectory.path,
);
if (result.exitCode != 0) {
throw Exception('flutter build failed: ${result.exitCode}\n${result.stderr}\n${result.stdout}');
}
for (final String buildMode in buildModes) {
expectDylibIsBundledWithFrameworks(exampleDirectory, buildMode, add2appBuildSubcommand.replaceAll('-framework', ''));
}
expectCCompilerIsConfigured(exampleDirectory);
});
});
}
}
/// For `flutter build` we can't easily test whether running the app works.
/// Check that we have the dylibs in the app.
void expectDylibIsBundledMacOS(Directory appDirectory, String buildMode) {
final Directory appBundle = appDirectory.childDirectory('build/$hostOs/Build/Products/${buildMode.upperCaseFirst()}/$exampleAppName.app');
expect(appBundle, exists);
final Directory frameworksFolder =
appBundle.childDirectory('Contents/Frameworks');
expect(frameworksFolder, exists);
// MyFramework.framework/
// MyFramework -> Versions/Current/MyFramework
// Resources -> Versions/Current/Resources
// Versions/
// A/
// MyFramework
// Resources/
// Info.plist
// Current -> A
final String frameworkName = packageName.substring(0, 15);
final Directory frameworkDir =
frameworksFolder.childDirectory('$frameworkName.framework');
final Directory versionsDir = frameworkDir.childDirectory('Versions');
final Directory versionADir = versionsDir.childDirectory('A');
final Directory resourcesDir = versionADir.childDirectory('Resources');
expect(resourcesDir, exists);
final File dylibFile = versionADir.childFile(frameworkName);
expect(dylibFile, exists);
final Link currentLink = versionsDir.childLink('Current');
expect(currentLink, exists);
expect(currentLink.resolveSymbolicLinksSync(), versionADir.path);
final Link resourcesLink = frameworkDir.childLink('Resources');
expect(resourcesLink, exists);
expect(resourcesLink.resolveSymbolicLinksSync(), resourcesDir.path);
final Link dylibLink = frameworkDir.childLink(frameworkName);
expect(dylibLink, exists);
expect(dylibLink.resolveSymbolicLinksSync(), dylibFile.path);
}
void expectDylibIsBundledIos(Directory appDirectory, String buildMode) {
final Directory appBundle = appDirectory.childDirectory('build/ios/${buildMode.upperCaseFirst()}-iphoneos/Runner.app');
expect(appBundle, exists);
final Directory frameworksFolder = appBundle.childDirectory('Frameworks');
expect(frameworksFolder, exists);
final String frameworkName = packageName.substring(0, 15);
final File dylib = frameworksFolder
.childDirectory('$frameworkName.framework')
.childFile(frameworkName);
expect(dylib, exists);
}
/// Checks that dylibs are bundled.
///
/// Sample path: build/linux/x64/release/bundle/lib/libmy_package.so
void expectDylibIsBundledLinux(Directory appDirectory, String buildMode) {
// Linux does not support cross compilation, so always only check current architecture.
final String architecture = Architecture.current.dartPlatform;
final Directory appBundle = appDirectory
.childDirectory('build')
.childDirectory(hostOs)
.childDirectory(architecture)
.childDirectory(buildMode)
.childDirectory('bundle');
expect(appBundle, exists);
final Directory dylibsFolder = appBundle.childDirectory('lib');
expect(dylibsFolder, exists);
final File dylib = dylibsFolder.childFile(OS.linux.dylibFileName(packageName));
expect(dylib, exists);
}
/// Checks that dylibs are bundled.
///
/// Sample path: build\windows\x64\runner\Debug\my_package_example.exe
void expectDylibIsBundledWindows(Directory appDirectory, String buildMode) {
// Linux does not support cross compilation, so always only check current architecture.
final String architecture = Architecture.current.dartPlatform;
final Directory appBundle = appDirectory
.childDirectory('build')
.childDirectory(hostOs)
.childDirectory(architecture)
.childDirectory('runner')
.childDirectory(buildMode.upperCaseFirst());
expect(appBundle, exists);
final File dylib = appBundle.childFile(OS.windows.dylibFileName(packageName));
expect(dylib, exists);
}
void expectDylibIsBundledAndroid(Directory appDirectory, String buildMode) {
final File apk = appDirectory
.childDirectory('build')
.childDirectory('app')
.childDirectory('outputs')
.childDirectory('flutter-apk')
.childFile('app-$buildMode.apk');
expect(apk, exists);
final OperatingSystemUtils osUtils = OperatingSystemUtils(
fileSystem: fileSystem,
logger: BufferLogger.test(),
platform: platform,
processManager: processManager,
);
final Directory apkUnzipped = appDirectory.childDirectory('apk-unzipped');
apkUnzipped.createSync();
osUtils.unzip(apk, apkUnzipped);
final Directory lib = apkUnzipped.childDirectory('lib');
for (final String arch in <String>['arm64-v8a', 'armeabi-v7a', 'x86_64']) {
final Directory archDir = lib.childDirectory(arch);
expect(archDir, exists);
// The dylibs should be next to the flutter and app so.
expect(archDir.childFile('libflutter.so'), exists);
if (buildMode != 'debug') {
expect(archDir.childFile('libapp.so'), exists);
}
final File dylib = archDir.childFile(OS.android.dylibFileName(packageName));
expect(dylib, exists);
}
}
/// For `flutter build` we can't easily test whether running the app works.
/// Check that we have the dylibs in the app.
void expectDylibIsBundledWithFrameworks(Directory appDirectory, String buildMode, String os) {
final Directory frameworksFolder = appDirectory.childDirectory('build/$os/framework/${buildMode.upperCaseFirst()}');
expect(frameworksFolder, exists);
final String frameworkName = packageName.substring(0, 15);
final File dylib = frameworksFolder
.childDirectory('$frameworkName.framework')
.childFile(frameworkName);
expect(dylib, exists);
}
/// Check that the native assets are built with the C Compiler that Flutter uses.
///
/// This inspects the build configuration to see if the C compiler was configured.
void expectCCompilerIsConfigured(Directory appDirectory) {
final Directory nativeAssetsBuilderDir = appDirectory.childDirectory('.dart_tool/native_assets_builder/');
for (final Directory subDir in nativeAssetsBuilderDir.listSync().whereType<Directory>()) {
final File config = subDir.childFile('config.yaml');
expect(config, exists);
final String contents = config.readAsStringSync();
// Dry run does not pass compiler info.
if (contents.contains('dry_run: true')) {
continue;
}
expect(contents, contains('cc: '));
}
}
extension on String {
String upperCaseFirst() {
return replaceFirst(this[0], this[0].toUpperCase());
}
}
Future<Directory> createTestProject(String packageName, Directory tempDirectory) async {
final ProcessResult result = processManager.runSync(
<String>[
flutterBin,
'create',
'--no-pub',
'--template=package_ffi',
packageName,
],
workingDirectory: tempDirectory.path,
);
if (result.exitCode != 0) {
throw Exception(
'flutter create failed: ${result.exitCode}\n${result.stderr}\n${result.stdout}',
);
}
final Directory packageDirectory = tempDirectory.childDirectory(packageName);
// No platform-specific boilerplate files.
expect(packageDirectory.childDirectory('android/'), isNot(exists));
expect(packageDirectory.childDirectory('ios/'), isNot(exists));
expect(packageDirectory.childDirectory('linux/'), isNot(exists));
expect(packageDirectory.childDirectory('macos/'), isNot(exists));
expect(packageDirectory.childDirectory('windows/'), isNot(exists));
await pinDependencies(packageDirectory.childFile('pubspec.yaml'));
await pinDependencies(
packageDirectory.childDirectory('example').childFile('pubspec.yaml'));
final ProcessResult result2 = await processManager.run(
<String>[
flutterBin,
'pub',
'get',
],
workingDirectory: packageDirectory.path,
);
expect(result2, const ProcessResultMatcher());
return packageDirectory;
}
Future<void> pinDependencies(File pubspecFile) async {
expect(pubspecFile, exists);
final String oldPubspec = await pubspecFile.readAsString();
final String newPubspec = oldPubspec.replaceAll(RegExp(r':\s*\^'), ': ');
expect(newPubspec, isNot(oldPubspec));
await pubspecFile.writeAsString(newPubspec);
}
Future<void> inTempDir(Future<void> Function(Directory tempDirectory) fun) async {
final Directory tempDirectory = fileSystem.directory(fileSystem.systemTempDirectory.createTempSync().resolveSymbolicLinksSync());
try {
await fun(tempDirectory);
} finally {
tryToDelete(tempDirectory);
}
}