flutter/packages/flutter_tools/test/general.shard/isolated/native_assets_test.dart
Martin Kustermann 01590aa27a
Refactor native asset integration into flutter tools (#158932)
Currently the `NativeAsset` target in flutter tools is responsible for
two things:

* performing the dart build (in the app as well as all transitive pub
dependencies)
* taking output (shared libraries) from this build and copying them
around

This intermingling of responsibilities leads to more complex code and
potentially unnecessary work: If the source code changed (e.g. `.c`
files change) we have to run the dart build again. But doing so may
result in the same shared libraries (e.g. adding comments to the `.c`
code). Currently we're going to copy the shared libraries despite them
having not changed, which then may cause upstream things to be dirtied
(if it's based on timestamp of files) and re-built.

Instead this PR splits this `NativeAsset` into the two orthogonal pieces

* `DartBuild` target that is responsible for the dart build
* `InstallCodeAssets` that is responsible for copying shared libraries
to the right place and producing a `native_assets.yaml`.

This decoupling is also preparation for a future where a dart build can
produce other kinds of assets (e.g. data assets) and is used in the web
build as well. (The web build would use `DartBuild` but not
`InstalCodeAssets`).
2024-11-15 21:04:42 +01:00

423 lines
15 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.
import 'package:file/file.dart';
import 'package:file/memory.dart';
import 'package:file_testing/file_testing.dart';
import 'package:flutter_tools/src/artifacts.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/build_system/build_system.dart';
import 'package:flutter_tools/src/build_system/targets/native_assets.dart';
import 'package:flutter_tools/src/features.dart';
import 'package:flutter_tools/src/globals.dart' as globals;
import 'package:flutter_tools/src/isolated/native_assets/native_assets.dart';
import 'package:native_assets_cli/code_assets_builder.dart' hide BuildMode;
import 'package:package_config/package_config_types.dart';
import '../../src/common.dart';
import '../../src/context.dart';
import '../../src/fakes.dart';
import 'fake_native_assets_build_runner.dart';
void main() {
late FakeProcessManager processManager;
late Environment environment;
late Artifacts artifacts;
late FileSystem fileSystem;
late BufferLogger logger;
late Uri projectUri;
setUp(() {
processManager = FakeProcessManager.empty();
logger = BufferLogger.test();
artifacts = Artifacts.test();
fileSystem = MemoryFileSystem.test();
environment = Environment.test(
fileSystem.currentDirectory,
inputs: <String, String>{},
artifacts: artifacts,
processManager: processManager,
fileSystem: fileSystem,
logger: logger,
);
environment.buildDir.createSync(recursive: true);
projectUri = environment.projectDir.uri;
});
testUsingContext('dry run with no package config', overrides: <Type, Generator>{
ProcessManager: () => FakeProcessManager.empty(),
}, () async {
expect(
await runFlutterSpecificDartDryRunOnPlatforms(
projectUri: projectUri,
fileSystem: fileSystem,
targetPlatforms: <TargetPlatform>[TargetPlatform.windows_x64],
buildRunner: FakeFlutterNativeAssetsBuildRunner(
hasPackageConfigResult: false,
),
),
null,
);
expect(
(globals.logger as BufferLogger).traceText,
contains('No package config found. Skipping native assets compilation.'),
);
});
testUsingContext('build with no package config', overrides: <Type, Generator>{
ProcessManager: () => FakeProcessManager.empty(),
}, () async {
await runFlutterSpecificDartBuild(
environmentDefines: <String, String>{
kBuildMode: BuildMode.debug.cliName,
},
targetPlatform: TargetPlatform.windows_x64,
projectUri: projectUri,
fileSystem: fileSystem,
buildRunner: FakeFlutterNativeAssetsBuildRunner(
hasPackageConfigResult: false,
),
);
expect(
(globals.logger as BufferLogger).traceText,
contains('No package config found. Skipping native assets compilation.'),
);
});
testUsingContext('dry run for multiple OSes with no package config', overrides: <Type, Generator>{
ProcessManager: () => FakeProcessManager.empty(),
}, () async {
await runFlutterSpecificDartDryRunOnPlatforms(
projectUri: projectUri,
fileSystem: fileSystem,
targetPlatforms: <TargetPlatform>[
TargetPlatform.windows_x64,
TargetPlatform.darwin,
TargetPlatform.ios,
],
buildRunner: FakeFlutterNativeAssetsBuildRunner(
hasPackageConfigResult: false,
),
);
expect(
(globals.logger as BufferLogger).traceText,
contains('No package config found. Skipping native assets compilation.'),
);
});
testUsingContext('dry run with assets but not enabled', overrides: <Type, Generator>{
ProcessManager: () => FakeProcessManager.empty(),
}, () async {
final File packageConfig = environment.projectDir.childFile('.dart_tool/package_config.json');
await packageConfig.parent.create();
await packageConfig.create();
expect(
() => runFlutterSpecificDartDryRunOnPlatforms(
projectUri: projectUri,
fileSystem: fileSystem,
targetPlatforms: <TargetPlatform>[TargetPlatform.windows_x64],
buildRunner: FakeFlutterNativeAssetsBuildRunner(
packagesWithNativeAssetsResult: <Package>[
Package('bar', projectUri),
],
),
),
throwsToolExit(
message: 'Package(s) bar require the native assets feature to be enabled. '
'Enable using `flutter config --enable-native-assets`.',
),
);
});
testUsingContext('dry run with assets', overrides: <Type, Generator>{
FeatureFlags: () => TestFeatureFlags(isNativeAssetsEnabled: true),
ProcessManager: () => FakeProcessManager.empty(),
}, () async {
final File packageConfig = environment.projectDir.childFile('.dart_tool/package_config.json');
await packageConfig.parent.create();
await packageConfig.create();
final FakeFlutterNativeAssetsBuildRunner buildRunner = FakeFlutterNativeAssetsBuildRunner(
packagesWithNativeAssetsResult: <Package>[
Package('bar', projectUri),
],
buildDryRunResult: FakeFlutterNativeAssetsBuilderResult.fromAssets(
codeAssets: <CodeAsset>[
CodeAsset(
package: 'bar',
name: 'bar.dart',
linkMode: DynamicLoadingBundled(),
os: OS.windows,
file: Uri.file('bar.dll'),
),
],
),
);
final Uri? nativeAssetsYaml = await runFlutterSpecificDartDryRunOnPlatforms(
projectUri: projectUri,
fileSystem: fileSystem,
targetPlatforms: <TargetPlatform>[TargetPlatform.windows_x64],
buildRunner: buildRunner,
);
expect(
(globals.logger as BufferLogger).traceText,
stringContainsInOrder(<String>[
'Dry running native assets for windows.',
'Dry running native assets for windows done.',
]),
);
expect(
nativeAssetsYaml,
projectUri.resolve('build/native_assets/windows/${InstallCodeAssets.nativeAssetsFilename}'),
);
expect(
await fileSystem.file(nativeAssetsYaml).readAsString(),
contains('package:bar/bar.dart'),
);
expect(buildRunner.buildDryRunInvocations, 1);
});
testUsingContext('Native assets: non-bundled libraries require no copying', overrides: <Type, Generator>{
FeatureFlags: () => TestFeatureFlags(isNativeAssetsEnabled: true),
ProcessManager: () => FakeProcessManager.empty(),
}, () async {
final File packageConfig =
environment.projectDir.childFile('.dart_tool/package_config.json');
final Uri nonFlutterTesterAssetUri = environment.buildDir.childFile('native_assets.yaml').uri;
await packageConfig.parent.create();
await packageConfig.create();
final File directSoFile = environment.projectDir.childFile('direct.so');
directSoFile.writeAsBytesSync(<int>[]);
CodeAsset makeCodeAsset(String name, LinkMode linkMode, [Uri? file])
=> CodeAsset(
package: 'bar',
name: name,
linkMode: linkMode,
os: OS.linux,
architecture: Architecture.x64,
file: file,
);
final Map<String, String> environmentDefines = <String, String>{
kBuildMode: BuildMode.release.cliName,
};
final List<CodeAsset> codeAssets = <CodeAsset>[
makeCodeAsset('malloc', LookupInProcess()),
makeCodeAsset('free', LookupInExecutable()),
makeCodeAsset('draw', DynamicLoadingSystem(Uri.file('/usr/lib/skia.so'))),
];
final DartBuildResult dartBuildResult = await runFlutterSpecificDartBuild(
environmentDefines: environmentDefines,
targetPlatform: TargetPlatform.linux_x64,
projectUri: projectUri,
fileSystem: fileSystem,
buildRunner: FakeFlutterNativeAssetsBuildRunner(
packagesWithNativeAssetsResult: <Package>[
Package('bar', projectUri),
],
buildResult: FakeFlutterNativeAssetsBuilderResult.fromAssets(codeAssets: codeAssets),
linkResult: FakeFlutterNativeAssetsBuilderResult.fromAssets(codeAssets: codeAssets),
),
);
await installCodeAssets(
dartBuildResult: dartBuildResult,
environmentDefines: environmentDefines,
targetPlatform: TargetPlatform.windows_x64,
projectUri: projectUri,
fileSystem: fileSystem,
nativeAssetsFileUri: nonFlutterTesterAssetUri,
);
expect(testLogger.traceText, isNot(contains('Copying native assets to')));
});
testUsingContext('build with assets but not enabled', overrides: <Type, Generator>{
ProcessManager: () => FakeProcessManager.empty(),
}, () async {
final File packageConfig = environment.projectDir.childFile('.dart_tool/package_config.json');
await packageConfig.parent.create();
await packageConfig.create();
expect(
() => runFlutterSpecificDartBuild(
environmentDefines: <String, String>{
kBuildMode: BuildMode.debug.cliName,
},
targetPlatform: TargetPlatform.windows_x64,
projectUri: projectUri,
fileSystem: fileSystem,
buildRunner: FakeFlutterNativeAssetsBuildRunner(
packagesWithNativeAssetsResult: <Package>[
Package('bar', projectUri),
],
),
),
throwsToolExit(
message: 'Package(s) bar require the native assets feature to be enabled. '
'Enable using `flutter config --enable-native-assets`.',
),
);
});
testUsingContext('build no assets', overrides: <Type, Generator>{
FeatureFlags: () => TestFeatureFlags(isNativeAssetsEnabled: true),
ProcessManager: () => FakeProcessManager.empty(),
}, () async {
final File packageConfig = environment.projectDir.childFile('.dart_tool/package_config.json');
final Uri nonFlutterTesterAssetUri = environment.buildDir.childFile(InstallCodeAssets.nativeAssetsFilename).uri;
await packageConfig.parent.create();
await packageConfig.create();
final Map<String, String> environmentDefines = <String, String>{
kBuildMode: BuildMode.debug.cliName,
};
final DartBuildResult dartBuildResult = await runFlutterSpecificDartBuild(
environmentDefines: environmentDefines,
targetPlatform: TargetPlatform.windows_x64,
projectUri: projectUri,
fileSystem: fileSystem,
buildRunner: FakeFlutterNativeAssetsBuildRunner(
packagesWithNativeAssetsResult: <Package>[
Package('bar', projectUri),
],
),
);
await installCodeAssets(
dartBuildResult: dartBuildResult,
environmentDefines: environmentDefines,
targetPlatform: TargetPlatform.windows_x64,
projectUri: projectUri,
fileSystem: fileSystem,
nativeAssetsFileUri: nonFlutterTesterAssetUri,
);
expect(
await fileSystem.file(nonFlutterTesterAssetUri).readAsString(),
isNot(contains('package:bar/bar.dart')),
);
expect(
environment.projectDir.childDirectory('build').childDirectory('native_assets').childDirectory('windows'),
exists,
);
});
testUsingContext('Native assets dry run error', overrides: <Type, Generator>{
FeatureFlags: () => TestFeatureFlags(isNativeAssetsEnabled: true),
ProcessManager: () => FakeProcessManager.empty(),
}, () async {
final File packageConfig =
environment.projectDir.childFile('.dart_tool/package_config.json');
await packageConfig.parent.create();
await packageConfig.create();
expect(
() => runFlutterSpecificDartDryRunOnPlatforms(
projectUri: projectUri,
fileSystem: fileSystem,
targetPlatforms: <TargetPlatform>[TargetPlatform.windows_x64],
buildRunner: FakeFlutterNativeAssetsBuildRunner(
packagesWithNativeAssetsResult: <Package>[
Package('bar', projectUri),
],
buildDryRunResult: null,
),
),
throwsToolExit(
message:
'Building (dry run) native assets failed. See the logs for more details.',
),
);
});
testUsingContext('Native assets build error', overrides: <Type, Generator>{
FeatureFlags: () => TestFeatureFlags(isNativeAssetsEnabled: true),
ProcessManager: () => FakeProcessManager.empty(),
}, () async {
final File packageConfig =
environment.projectDir.childFile('.dart_tool/package_config.json');
await packageConfig.parent.create();
await packageConfig.create();
expect(
() => runFlutterSpecificDartBuild(
environmentDefines: <String, String>{
kBuildMode: BuildMode.debug.cliName,
},
targetPlatform: TargetPlatform.linux_x64,
projectUri: projectUri,
fileSystem: fileSystem,
buildRunner: FakeFlutterNativeAssetsBuildRunner(
packagesWithNativeAssetsResult: <Package>[
Package('bar', projectUri),
],
buildResult: null,
),
),
throwsToolExit(
message:
'Building native assets failed. See the logs for more details.',
),
);
});
testUsingContext('Native assets: no duplicate assets with linking', overrides: <Type, Generator>{
FeatureFlags: () => TestFeatureFlags(isNativeAssetsEnabled: true),
ProcessManager: () => FakeProcessManager.empty(),
}, () async {
final File packageConfig =
environment.projectDir.childFile('.dart_tool/package_config.json');
await packageConfig.parent.create();
await packageConfig.create();
final File directSoFile = environment.projectDir.childFile('direct.so');
directSoFile.writeAsBytesSync(<int>[]);
final File linkableAFile = environment.projectDir.childFile('linkable.a');
linkableAFile.writeAsBytesSync(<int>[]);
final File linkedSoFile = environment.projectDir.childFile('linked.so');
linkedSoFile.writeAsBytesSync(<int>[]);
CodeAsset makeCodeAsset(String name, Uri file, LinkMode linkMode)
=> CodeAsset(
package: 'bar',
name: name,
linkMode: linkMode,
os: OS.linux,
architecture: Architecture.x64,
file: file,
);
final DartBuildResult result = await runFlutterSpecificDartBuild(
environmentDefines: <String, String>{
// Release mode means the dart build has linking enabled.
kBuildMode: BuildMode.release.cliName,
},
targetPlatform: TargetPlatform.linux_x64,
projectUri: projectUri,
fileSystem: fileSystem,
buildRunner: FakeFlutterNativeAssetsBuildRunner(
packagesWithNativeAssetsResult: <Package>[
Package('bar', projectUri),
],
buildResult: FakeFlutterNativeAssetsBuilderResult.fromAssets(
codeAssets: <CodeAsset>[
makeCodeAsset('direct', directSoFile.uri, DynamicLoadingBundled()),
],
codeAssetsForLinking: <String, List<CodeAsset>>{
'package:bar' : <CodeAsset>[
makeCodeAsset('linkable', linkableAFile.uri, StaticLinking()),
],
},
),
linkResult: FakeFlutterNativeAssetsBuilderResult.fromAssets(
codeAssets: <CodeAsset>[
makeCodeAsset('direct', directSoFile.uri, DynamicLoadingBundled()),
makeCodeAsset('linked', linkedSoFile.uri, DynamicLoadingBundled()),
],
),
),
);
expect(
result.codeAssets.map((CodeAsset c) => c.file!.toString()).toList()..sort(),
<String>[directSoFile.uri.toString(), linkedSoFile.uri.toString()]);
});
}