Moritz 661b8edef2
Add data assets (#174685)
Refiling of #169273 (reverted in
https://github.com/flutter/flutter/pull/170034), which is a refiling of
#164094, which itself is a rebase of #159675.

This PR adds bundling support for the experimental dart data asset
feature: Dart packages with hooks can now emit data assets which the
flutter tool will bundle.

It relies on flutter's existing asset bundling mechanism (e.g. entries
in AssetManifest.json, DevFS syncing in reload/restart, ...).

The support is added under an experimental flag (similar to the existing
native assets experimental flag).

Also, kNativeAssets is removed to also bundle data assets on flutter
build bundle.

The chrome sandbox is disabled as per #165664.

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
2025-09-02 23:00:30 +00:00

226 lines
7.7 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:meta/meta.dart';
import 'package:pool/pool.dart';
import 'package:process/process.dart';
import 'artifacts.dart';
import 'asset.dart' hide defaultManifestPath;
import 'base/common.dart';
import 'base/file_system.dart';
import 'base/logger.dart';
import 'build_info.dart';
import 'build_system/build_system.dart';
import 'build_system/depfile.dart';
import 'build_system/tools/asset_transformer.dart';
import 'build_system/tools/shader_compiler.dart';
import 'bundle.dart';
import 'cache.dart';
import 'devfs.dart';
import 'device.dart';
import 'globals.dart' as globals;
import 'project.dart';
/// Provides a `build` method that builds the bundle.
class BundleBuilder {
/// Builds the bundle for the given target platform.
///
/// The default `mainPath` is `lib/main.dart`.
/// The default `manifestPath` is `pubspec.yaml`.
Future<void> build({
required TargetPlatform platform,
required BuildInfo buildInfo,
FlutterProject? project,
String? mainPath,
String manifestPath = defaultManifestPath,
String? applicationKernelFilePath,
String? depfilePath,
String? assetDirPath,
@visibleForTesting BuildSystem? buildSystem,
}) async {
project ??= FlutterProject.current();
mainPath ??= defaultMainPath;
depfilePath ??= defaultDepfilePath;
assetDirPath ??= getAssetBuildDirectory();
buildSystem ??= globals.buildSystem;
// If the precompiled flag was not passed, force us into debug mode.
final environment = Environment(
projectDir: project.directory,
packageConfigPath: buildInfo.packageConfigPath,
outputDir: globals.fs.directory(assetDirPath),
buildDir: project.dartTool.childDirectory('flutter_build'),
cacheDir: globals.cache.getRoot(),
flutterRootDir: globals.fs.directory(Cache.flutterRoot),
engineVersion: globals.artifacts!.usesLocalArtifacts
? null
: globals.flutterVersion.engineRevision,
defines: <String, String>{
// used by the KernelSnapshot target
kTargetPlatform: getNameForTargetPlatform(platform),
kTargetFile: mainPath,
kDeferredComponents: 'false',
...buildInfo.toBuildSystemEnvironment(),
},
artifacts: globals.artifacts!,
fileSystem: globals.fs,
logger: globals.logger,
processManager: globals.processManager,
analytics: globals.analytics,
platform: globals.platform,
generateDartPluginRegistry: true,
);
final Target target = buildInfo.mode == BuildMode.debug
? globals.buildTargets.copyFlutterBundle
: globals.buildTargets.releaseCopyFlutterBundle;
final BuildResult result = await buildSystem.build(target, environment);
if (!result.success) {
for (final ExceptionMeasurement measurement in result.exceptions.values) {
globals.printError(
'Target ${measurement.target} failed: ${measurement.exception}',
stackTrace: measurement.fatal ? measurement.stackTrace : null,
);
}
throwToolExit('Failed to build bundle.');
}
final depfile = Depfile(result.inputFiles, result.outputFiles);
final File outputDepfile = globals.fs.file(depfilePath);
if (!outputDepfile.parent.existsSync()) {
outputDepfile.parent.createSync(recursive: true);
}
environment.depFileService.writeToFile(depfile, outputDepfile);
// Work around for flutter_tester placing kernel artifacts in odd places.
if (applicationKernelFilePath != null) {
final File outputDill = globals.fs.directory(assetDirPath).childFile('kernel_blob.bin');
if (outputDill.existsSync()) {
outputDill.copySync(applicationKernelFilePath);
}
}
return;
}
}
Future<AssetBundle?> buildAssets({
required String manifestPath,
String? assetDirPath,
required String packageConfigPath,
TargetPlatform? targetPlatform,
String? flavor,
}) async {
assetDirPath ??= getAssetBuildDirectory();
// Build the asset bundle.
final AssetBundle assetBundle = AssetBundleFactory.instance.createBundle();
final int result = await assetBundle.build(
manifestPath: manifestPath,
packageConfigPath: packageConfigPath,
targetPlatform: targetPlatform,
flavor: flavor,
);
if (result != 0) {
return null;
}
return assetBundle;
}
Future<void> writeBundle(
Directory bundleDir,
Map<String, AssetBundleEntry> assetEntries, {
required TargetPlatform targetPlatform,
required ImpellerStatus impellerStatus,
required ProcessManager processManager,
required FileSystem fileSystem,
required Artifacts artifacts,
required Logger logger,
required Directory projectDir,
required BuildMode buildMode,
}) async {
if (bundleDir.existsSync()) {
try {
bundleDir.deleteSync(recursive: true);
} on FileSystemException catch (err) {
logger.printWarning(
'Failed to clean up asset directory ${bundleDir.path}: $err\n'
'To clean build artifacts, use the command "flutter clean".',
);
}
}
bundleDir.createSync(recursive: true);
final shaderCompiler = ShaderCompiler(
processManager: processManager,
logger: logger,
fileSystem: fileSystem,
artifacts: artifacts,
);
final assetTransformer = AssetTransformer(
processManager: processManager,
fileSystem: fileSystem,
dartBinaryPath: artifacts.getArtifactPath(Artifact.engineDartBinary),
buildMode: buildMode,
);
// Limit number of open files to avoid running out of file descriptors.
final pool = Pool(64);
await Future.wait<void>(
assetEntries.entries.map<Future<void>>((MapEntry<String, AssetBundleEntry> entry) async {
final PoolResource resource = await pool.request();
try {
// This will result in strange looking files, for example files with `/`
// on Windows or files that end up getting URI encoded such as `#.ext`
// to `%23.ext`. However, we have to keep it this way since the
// platform channels in the framework will URI encode these values,
// and the native APIs will look for files this way.
final File file = fileSystem.file(fileSystem.path.join(bundleDir.path, entry.key));
file.parent.createSync(recursive: true);
final DevFSContent devFSContent = entry.value.content;
if (devFSContent is DevFSFileContent) {
final input = devFSContent.file as File;
var doCopy = true;
switch (entry.value.kind) {
case AssetKind.regular:
if (entry.value.transformers.isEmpty) {
break;
}
final AssetTransformationFailure? failure = await assetTransformer.transformAsset(
asset: input,
outputPath: file.path,
workingDirectory: projectDir.path,
transformerEntries: entry.value.transformers,
logger: logger,
);
doCopy = false;
if (failure != null) {
throwToolExit(
'User-defined transformation of asset "${entry.key}" failed.\n'
'${failure.message}',
);
}
case AssetKind.font:
break;
case AssetKind.shader:
doCopy = !await shaderCompiler.compileShader(
input: input,
outputPath: file.path,
targetPlatform: targetPlatform,
);
}
if (doCopy) {
input.copySync(file.path);
}
} else {
await file.writeAsBytes(await entry.value.contentsAsBytes());
}
} finally {
resource.release();
}
}),
);
}