mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
## Description
This PR introduces platform-specific asset support in `pubspec.yaml`.
Currently, Flutter does not allow specifying which platforms an asset
should be included for.
This results in all declared assets being bundled for every target
platform, even if some are irrelevant (e.g. desktop-only or mobile-only
images).
### What this PR changes
- Adds a new optional `platforms` field under each asset in
`pubspec.yaml`.
- The field accepts a list of strings (platform identifiers, e.g.
`["android", "ios", "web", "windows", "macos", "linux"]`).
- Assets with a `platforms` restriction are only included in the bundle
when building for a matching platform.
- Invalid values (non-strings or unknown platform names) log an error.
### Example
```yaml
flutter:
assets:
- path: assets/logo.png
- path: assets/web_worker.js
platforms: [web]
- path: assets/desktop_icon.png
platforms: [windows, linux, macos]
```
#### Before
All assets (`logo.png`, `web_worker.js`, `desktop_icon.png`) are bundled
into **every build**, regardless of platform.
#### After
- `logo.png` is included on all platforms.
- `web_worker.js` is included only on web builds.
- `desktop_icon.png` is included only on desktop builds.
### Why this is useful
This significantly improves bundle size, prevents unused resources from
being shipped, and gives developers better control over asset
management.
## Issues
Fixes #65065
## Reviewer note
Would a design document be helpful for this change, or is the current
explanation sufficient?
## 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.
<!-- 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
226 lines
7.7 KiB
Dart
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,
|
|
required 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();
|
|
}
|
|
}),
|
|
);
|
|
}
|