From 5239c8db4891db3a32eb56f7729bf03ee1b81cfd Mon Sep 17 00:00:00 2001 From: Alex Frei <40503456+hm21@users.noreply.github.com> Date: Tue, 11 Nov 2025 19:50:05 +0100 Subject: [PATCH] feat(assets): add platform-specific asset filtering in pubspec.yaml (#176393) ## 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. [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 --- packages/flutter_tools/lib/src/asset.dart | 65 ++++++-- .../lib/src/base/deferred_component.dart | 3 + .../lib/src/build_system/targets/assets.dart | 7 +- .../flutter_tools/lib/src/bundle_builder.dart | 2 +- .../flutter_tools/lib/src/commands/test.dart | 1 + .../lib/src/flutter_manifest.dart | 54 +++++- packages/flutter_tools/lib/src/run_hot.dart | 1 + .../preview_pubspec_builder.dart | 1 + .../asset_bundle_flavors_test.dart | 2 + .../asset_bundle_package_fonts_test.dart | 11 +- .../asset_bundle_package_test.dart | 27 ++- .../test/general.shard/asset_bundle_test.dart | 144 +++++++++++++--- .../asset_bundle_variant_test.dart | 6 + .../test/general.shard/asset_test.dart | 5 + .../build_system/targets/assets_test.dart | 155 ++++++++++++++++++ .../general.shard/flutter_manifest_test.dart | 76 +++++++++ 16 files changed, 515 insertions(+), 45 deletions(-) diff --git a/packages/flutter_tools/lib/src/asset.dart b/packages/flutter_tools/lib/src/asset.dart index 53b823373ef..0c220f8f0f5 100644 --- a/packages/flutter_tools/lib/src/asset.dart +++ b/packages/flutter_tools/lib/src/asset.dart @@ -195,7 +195,7 @@ abstract class AssetBundle { String manifestPath = defaultManifestPath, required String packageConfigPath, bool deferredComponentsEnabled = false, - TargetPlatform? targetPlatform, + required TargetPlatform targetPlatform, String? flavor, bool includeAssetsFromDevDependencies = false, }); @@ -326,7 +326,7 @@ class ManifestAssetBundle implements AssetBundle { FlutterProject? flutterProject, required String packageConfigPath, bool deferredComponentsEnabled = false, - TargetPlatform? targetPlatform, + required TargetPlatform targetPlatform, String? flavor, bool includeAssetsFromDevDependencies = false, }) async { @@ -402,6 +402,7 @@ class ManifestAssetBundle implements AssetBundle { assetBasePath, wildcardDirectories, flutterProject.directory, + targetPlatform: targetPlatform, flavor: flavor, ); if (!_splitDeferredAssets || !deferredComponentsEnabled) { @@ -716,7 +717,7 @@ class ManifestAssetBundle implements AssetBundle { } } - void _setLicenseIfChanged(String combinedLicenses, TargetPlatform? targetPlatform) { + void _setLicenseIfChanged(String combinedLicenses, TargetPlatform targetPlatform) { // On the web, don't compress the NOTICES file since the client doesn't have // dart:io to decompress it. So use the standard _setIfChanged to check if // the strings still match. @@ -838,6 +839,7 @@ class ManifestAssetBundle implements AssetBundle { String assetBasePath, List wildcardDirectories, Directory projectDirectory, { + required TargetPlatform targetPlatform, String? flavor, }) { final List? components = flutterManifest.deferredComponents; @@ -859,6 +861,7 @@ class ManifestAssetBundle implements AssetBundle { componentAssets, assetsEntry.uri, flavors: assetsEntry.flavors, + platforms: assetsEntry.platforms, transformers: assetsEntry.transformers, ); } else { @@ -870,13 +873,15 @@ class ManifestAssetBundle implements AssetBundle { componentAssets, assetsEntry.uri, flavors: assetsEntry.flavors, + platforms: assetsEntry.platforms, transformers: assetsEntry.transformers, ); } } componentAssets.removeWhere( - (_Asset asset, List<_Asset> variants) => !asset.matchesFlavor(flavor), + (_Asset asset, List<_Asset> variants) => + !asset.matchesFlavor(flavor) || !asset.matchesPlatform(targetPlatform), ); deferredComponentsAssetVariants[component.name] = componentAssets; } @@ -1018,7 +1023,7 @@ class ManifestAssetBundle implements AssetBundle { FlutterManifest flutterManifest, List wildcardDirectories, String assetBase, - TargetPlatform? targetPlatform, { + TargetPlatform targetPlatform, { String? packageName, Package? attributedPackage, required String? flavor, @@ -1039,6 +1044,7 @@ class ManifestAssetBundle implements AssetBundle { packageName: packageName, attributedPackage: attributedPackage, flavors: assetsEntry.flavors, + platforms: assetsEntry.platforms, transformers: assetsEntry.transformers, ); } else { @@ -1052,6 +1058,7 @@ class ManifestAssetBundle implements AssetBundle { packageName: packageName, attributedPackage: attributedPackage, flavors: assetsEntry.flavors, + platforms: assetsEntry.platforms, transformers: assetsEntry.transformers, ); } @@ -1066,6 +1073,15 @@ class ManifestAssetBundle implements AssetBundle { ); return true; } + if (!asset.matchesPlatform(targetPlatform)) { + _logger.printTrace( + 'Skipping assets entry "${asset.entryUri.path}" since ' + 'its configured platform(s) did not match the target platform.\n' + 'Configured platforms: ${asset.platforms.join(', ')}\n' + 'Target platform: ${targetPlatform.osName}\n', + ); + return true; + } return false; }); @@ -1101,6 +1117,7 @@ class ManifestAssetBundle implements AssetBundle { attributedPackage: attributedPackage, assetKind: AssetKind.shader, flavors: {}, + platforms: {}, transformers: [], ); } @@ -1116,6 +1133,7 @@ class ManifestAssetBundle implements AssetBundle { attributedPackage, assetKind: AssetKind.font, flavors: {}, + platforms: {}, transformers: [], ); final File baseAssetFile = baseAsset.lookupAssetFile(_fileSystem); @@ -1142,6 +1160,7 @@ class ManifestAssetBundle implements AssetBundle { String? packageName, Package? attributedPackage, required Set flavors, + required Set platforms, required List transformers, }) { final String directoryPath; @@ -1174,6 +1193,7 @@ class ManifestAssetBundle implements AssetBundle { attributedPackage: attributedPackage, originUri: assetUri, flavors: flavors, + platforms: platforms, transformers: transformers, ); } @@ -1191,6 +1211,7 @@ class ManifestAssetBundle implements AssetBundle { Package? attributedPackage, AssetKind assetKind = AssetKind.regular, required Set flavors, + required Set platforms, required List transformers, }) { final _Asset asset = _resolveAsset( @@ -1202,6 +1223,7 @@ class ManifestAssetBundle implements AssetBundle { assetKind: assetKind, originUri: originUri, flavors: flavors, + platforms: platforms, transformers: transformers, ); @@ -1225,6 +1247,7 @@ class ManifestAssetBundle implements AssetBundle { package: attributedPackage, kind: assetKind, flavors: flavors, + platforms: platforms, transformers: transformers, ), ); @@ -1335,6 +1358,7 @@ class ManifestAssetBundle implements AssetBundle { Uri? originUri, AssetKind assetKind = AssetKind.regular, required Set flavors, + required Set platforms, required List transformers, }) { _ensureAssetPathIsValid(assetsBaseDir: assetsBaseDir, assetUri: assetUri); @@ -1351,6 +1375,7 @@ class ManifestAssetBundle implements AssetBundle { assetKind: assetKind, originUri: originUri, flavors: flavors, + platforms: platforms, transformers: transformers, ); if (packageAsset != null) { @@ -1370,6 +1395,7 @@ class ManifestAssetBundle implements AssetBundle { originUri: originUri, kind: assetKind, flavors: flavors, + platforms: platforms, transformers: transformers, ); } @@ -1381,6 +1407,7 @@ class ManifestAssetBundle implements AssetBundle { AssetKind assetKind = AssetKind.regular, Uri? originUri, Set? flavors, + Set? platforms, List? transformers, }) { assert(assetUri.pathSegments.first == 'packages'); @@ -1397,6 +1424,7 @@ class ManifestAssetBundle implements AssetBundle { kind: assetKind, originUri: originUri, flavors: flavors, + platforms: platforms, transformers: transformers, ); } @@ -1420,9 +1448,11 @@ class _Asset { required this.package, this.kind = AssetKind.regular, Set? flavors, + Set? platforms, List? transformers, }) : originUri = originUri ?? entryUri, flavors = flavors ?? const {}, + platforms = platforms ?? const {}, transformers = transformers ?? const []; final String baseDir; @@ -1444,6 +1474,8 @@ class _Asset { final Set flavors; + final Set platforms; + final List transformers; File lookupAssetFile(FileSystem fileSystem) { @@ -1472,11 +1504,20 @@ class _Asset { return flavors.contains(flavor); } + bool matchesPlatform(TargetPlatform targetPlatform) { + if (platforms.isEmpty || targetPlatform == TargetPlatform.tester) { + return true; + } + + return platforms.contains(targetPlatform.osName); + } + bool hasEquivalentFlavorsWith(_Asset other) { - final Set assetFlavors = flavors.toSet(); - final Set otherFlavors = other.flavors.toSet(); - return assetFlavors.length == otherFlavors.length && - assetFlavors.every((String e) => otherFlavors.contains(e)); + return setEquals(flavors, other.flavors); + } + + bool hasEquivalentPlatformsWith(_Asset other) { + return setEquals(platforms, other.platforms); } @override @@ -1495,11 +1536,13 @@ class _Asset { other.relativeUri == relativeUri && other.entryUri == entryUri && other.kind == kind && - hasEquivalentFlavorsWith(other); + hasEquivalentFlavorsWith(other) && + hasEquivalentPlatformsWith(other); } @override - int get hashCode => Object.hashAll([baseDir, relativeUri, entryUri, kind, ...flavors]); + int get hashCode => + Object.hashAll([baseDir, relativeUri, entryUri, kind, ...flavors, ...platforms]); } // Given an assets directory like this: diff --git a/packages/flutter_tools/lib/src/base/deferred_component.dart b/packages/flutter_tools/lib/src/base/deferred_component.dart index 06d0c9f7c0d..1066f060d86 100644 --- a/packages/flutter_tools/lib/src/base/deferred_component.dart +++ b/packages/flutter_tools/lib/src/base/deferred_component.dart @@ -108,6 +108,9 @@ class DeferredComponent { if (asset.flavors.isNotEmpty) { out.write(' (flavors: ${asset.flavors.join(', ')})'); } + if (asset.platforms.isNotEmpty) { + out.write(' (platforms: ${asset.platforms.join(', ')})'); + } } return out.toString(); } diff --git a/packages/flutter_tools/lib/src/build_system/targets/assets.dart b/packages/flutter_tools/lib/src/build_system/targets/assets.dart index be7cef81035..f9ebbae2045 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/assets.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/assets.dart @@ -269,7 +269,10 @@ class CopyAssets extends Target { List get depfiles => const ['flutter_assets.d']; @override - Future build(Environment environment) async { + Future build( + Environment environment, { + TargetPlatform targetPlatform = TargetPlatform.android, + }) async { final String? buildModeEnvironment = environment.defines[kBuildMode]; if (buildModeEnvironment == null) { throw MissingDefineException(kBuildMode, name); @@ -282,7 +285,7 @@ class CopyAssets extends Target { environment, output, dartHookResult: dartHookResult, - targetPlatform: TargetPlatform.android, + targetPlatform: targetPlatform, buildMode: buildMode, flavor: environment.defines[kFlavor], additionalContent: { diff --git a/packages/flutter_tools/lib/src/bundle_builder.dart b/packages/flutter_tools/lib/src/bundle_builder.dart index 13d09c75c49..e343c884924 100644 --- a/packages/flutter_tools/lib/src/bundle_builder.dart +++ b/packages/flutter_tools/lib/src/bundle_builder.dart @@ -108,7 +108,7 @@ Future buildAssets({ required String manifestPath, String? assetDirPath, required String packageConfigPath, - TargetPlatform? targetPlatform, + required TargetPlatform targetPlatform, String? flavor, }) async { assetDirPath ??= getAssetBuildDirectory(); diff --git a/packages/flutter_tools/lib/src/commands/test.dart b/packages/flutter_tools/lib/src/commands/test.dart index 6d59f4eb783..57562f6ac79 100644 --- a/packages/flutter_tools/lib/src/commands/test.dart +++ b/packages/flutter_tools/lib/src/commands/test.dart @@ -769,6 +769,7 @@ class TestCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts { packageConfigPath: packageConfigPath, flavor: flavor, includeAssetsFromDevDependencies: true, + targetPlatform: TargetPlatform.tester, ); if (build != 0) { throwToolExit('Error: Failed to build asset bundle'); diff --git a/packages/flutter_tools/lib/src/flutter_manifest.dart b/packages/flutter_tools/lib/src/flutter_manifest.dart index 92e8fe61472..1d1ab6b8ff0 100644 --- a/packages/flutter_tools/lib/src/flutter_manifest.dart +++ b/packages/flutter_tools/lib/src/flutter_manifest.dart @@ -788,20 +788,23 @@ class AssetsEntry { const AssetsEntry({ required this.uri, this.flavors = const {}, + this.platforms = const {}, this.transformers = const [], }); final Uri uri; final Set flavors; + final Set platforms; final List transformers; Object? get descriptor { - if (transformers.isEmpty && flavors.isEmpty) { + if (transformers.isEmpty && flavors.isEmpty && platforms.isEmpty) { return uri.toString(); } return { _pathKey: uri.toString(), if (flavors.isNotEmpty) _flavorKey: flavors.toList(), + if (platforms.isNotEmpty) _platformsKey: platforms.toList(), if (transformers.isNotEmpty) _transformersKey: transformers.map((AssetTransformerEntry e) => e.descriptor).toList(), }; @@ -809,6 +812,7 @@ class AssetsEntry { static const _pathKey = 'path'; static const _flavorKey = 'flavors'; + static const _platformsKey = 'platforms'; static const _transformersKey = 'transformers'; static AssetsEntry? parseFromYaml(Object? yaml) { @@ -856,11 +860,15 @@ class AssetsEntry { final (List? flavors, List flavorsErrors) = _parseFlavorsSection( yaml[_flavorKey], ); + final (List? platforms, List platformsErrors) = _parsePlatformsSection( + yaml[_platformsKey], + ); final (List? transformers, List transformersErrors) = _parseTransformersSection(yaml[_transformersKey]); final errors = [ ...flavorsErrors.map((String e) => 'In $_flavorKey section of asset "$path": $e'), + ...platformsErrors.map((String e) => 'In $_platformsKey section of asset "$path": $e'), ...transformersErrors.map( (String e) => 'In $_transformersKey section of asset "$path": $e', ), @@ -873,6 +881,7 @@ class AssetsEntry { AssetsEntry( uri: Uri(pathSegments: path.split('/')), flavors: Set.from(flavors ?? []), + platforms: Set.from(platforms ?? []), transformers: transformers ?? [], ), null, @@ -894,6 +903,41 @@ class AssetsEntry { return _parseList(yaml, _flavorKey, 'String'); } + /// Parses and validates the "platforms" section of an asset entry in pubspec.yaml. + /// + /// Returns a tuple containing the parsed platforms list and any validation errors. + /// If errors are encountered, the platforms list will be null and errors will be non-empty. + static (List? platforms, List errors) _parsePlatformsSection(Object? yaml) { + if (yaml == null) { + return (null, []); + } + + final (List? platforms, List errors) = _parseList( + yaml, + _platformsKey, + 'String', + ); + + if (errors.isNotEmpty) { + return (null, errors); + } + + if (platforms != null) { + final Set invalidPlatforms = platforms.toSet().difference(_kValidPluginPlatforms); + + if (invalidPlatforms.isNotEmpty) { + return ( + null, + [ + 'Invalid platform(s): "${invalidPlatforms.join(", ")}". Supported platforms are: "${_kValidPluginPlatforms.join(", ")}".', + ], + ); + } + } + + return (platforms, errors); + } + static (List?, List errors) _parseTransformersSection( Object? yaml, ) { @@ -934,18 +978,22 @@ class AssetsEntry { return false; } - return uri == other.uri && setEquals(flavors, other.flavors); + return uri == other.uri && + setEquals(flavors, other.flavors) && + setEquals(platforms, other.platforms); } @override int get hashCode => Object.hashAll([ uri.hashCode, Object.hashAllUnordered(flavors), + Object.hashAllUnordered(platforms), Object.hashAll(transformers), ]); @override - String toString() => 'AssetsEntry(uri: $uri, flavors: $flavors, transformers: $transformers)'; + String toString() => + 'AssetsEntry(uri: $uri, flavors: $flavors, platforms: $platforms, transformers: $transformers)'; } /// Represents an entry in the "transformers" section of an asset. diff --git a/packages/flutter_tools/lib/src/run_hot.dart b/packages/flutter_tools/lib/src/run_hot.dart index b22c97e42d0..eb2bbece0a0 100644 --- a/packages/flutter_tools/lib/src/run_hot.dart +++ b/packages/flutter_tools/lib/src/run_hot.dart @@ -499,6 +499,7 @@ class HotRunner extends ResidentRunner { ), packageConfigPath: debuggingOptions.buildInfo.packageConfigPath, flavor: debuggingOptions.buildInfo.flavor, + targetPlatform: targetPlatform, ); if (result != 0) { return UpdateFSReport(); diff --git a/packages/flutter_tools/lib/src/widget_preview/preview_pubspec_builder.dart b/packages/flutter_tools/lib/src/widget_preview/preview_pubspec_builder.dart index 082f875f3c5..3e8d3689857 100644 --- a/packages/flutter_tools/lib/src/widget_preview/preview_pubspec_builder.dart +++ b/packages/flutter_tools/lib/src/widget_preview/preview_pubspec_builder.dart @@ -76,6 +76,7 @@ class PreviewPubspecBuilder { return AssetsEntry( uri: transformAssetUri(asset.uri), flavors: asset.flavors, + platforms: asset.platforms, transformers: asset.transformers, ); } diff --git a/packages/flutter_tools/test/general.shard/asset_bundle_flavors_test.dart b/packages/flutter_tools/test/general.shard/asset_bundle_flavors_test.dart index 93dd92594ff..ac44f4319ad 100644 --- a/packages/flutter_tools/test/general.shard/asset_bundle_flavors_test.dart +++ b/packages/flutter_tools/test/general.shard/asset_bundle_flavors_test.dart @@ -8,6 +8,7 @@ import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/platform.dart'; import 'package:flutter_tools/src/base/user_messages.dart'; +import 'package:flutter_tools/src/build_info.dart'; import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/project.dart'; @@ -37,6 +38,7 @@ void main() { packageConfigPath: '.dart_tool/package_config.json', flutterProject: FlutterProject.fromDirectoryTest(fileSystem.currentDirectory), flavor: flavor, + targetPlatform: TargetPlatform.tester, ); return bundle; } diff --git a/packages/flutter_tools/test/general.shard/asset_bundle_package_fonts_test.dart b/packages/flutter_tools/test/general.shard/asset_bundle_package_fonts_test.dart index edac9897a97..e4f3c165477 100644 --- a/packages/flutter_tools/test/general.shard/asset_bundle_package_fonts_test.dart +++ b/packages/flutter_tools/test/general.shard/asset_bundle_package_fonts_test.dart @@ -9,6 +9,7 @@ import 'package:file/memory.dart'; import 'package:flutter_tools/src/asset.dart'; import 'package:flutter_tools/src/base/file_system.dart'; +import 'package:flutter_tools/src/build_info.dart'; import 'package:flutter_tools/src/globals.dart' as globals; @@ -62,7 +63,10 @@ $fontsSection String expectedAssetManifest, ) async { final AssetBundle bundle = AssetBundleFactory.instance.createBundle(); - await bundle.build(packageConfigPath: '.dart_tool/package_config.json'); + await bundle.build( + packageConfigPath: '.dart_tool/package_config.json', + targetPlatform: TargetPlatform.tester, + ); for (final packageName in packages) { for (final packageFont in packageFonts) { @@ -114,7 +118,10 @@ $fontsSection writePubspecFile('p/p/pubspec.yaml', 'test_package'); final AssetBundle bundle = AssetBundleFactory.instance.createBundle(); - await bundle.build(packageConfigPath: '.dart_tool/package_config.json'); + await bundle.build( + packageConfigPath: '.dart_tool/package_config.json', + targetPlatform: TargetPlatform.tester, + ); expect( bundle.entries.keys, unorderedEquals(['AssetManifest.bin', 'FontManifest.json', 'NOTICES.Z']), diff --git a/packages/flutter_tools/test/general.shard/asset_bundle_package_test.dart b/packages/flutter_tools/test/general.shard/asset_bundle_package_test.dart index 16157b0a56d..c800e0d0027 100644 --- a/packages/flutter_tools/test/general.shard/asset_bundle_package_test.dart +++ b/packages/flutter_tools/test/general.shard/asset_bundle_package_test.dart @@ -10,6 +10,7 @@ import 'package:file/memory.dart'; import 'package:flutter_tools/src/asset.dart'; import 'package:flutter_tools/src/base/file_system.dart'; +import 'package:flutter_tools/src/build_info.dart'; import 'package:flutter_tools/src/globals.dart' as globals; import 'package:standard_message_codec/standard_message_codec.dart'; @@ -83,7 +84,11 @@ $assetsSection String? flavor, }) async { final AssetBundle bundle = AssetBundleFactory.instance.createBundle(); - await bundle.build(packageConfigPath: '.dart_tool/package_config.json', flavor: flavor); + await bundle.build( + packageConfigPath: '.dart_tool/package_config.json', + flavor: flavor, + targetPlatform: TargetPlatform.tester, + ); for (final packageName in packages) { for (final asset in assets) { @@ -138,7 +143,10 @@ $assetsSection writePubspecFile('p/p/pubspec.yaml', 'test_package'); final AssetBundle bundle = AssetBundleFactory.instance.createBundle(); - await bundle.build(packageConfigPath: '.dart_tool/package_config.json'); + await bundle.build( + packageConfigPath: '.dart_tool/package_config.json', + targetPlatform: TargetPlatform.tester, + ); expect( bundle.entries.keys, unorderedEquals(['NOTICES.Z', 'AssetManifest.bin', 'FontManifest.json']), @@ -166,7 +174,10 @@ $assetsSection writeAssets('p/p/', assets); final AssetBundle bundle = AssetBundleFactory.instance.createBundle(); - await bundle.build(packageConfigPath: '.dart_tool/package_config.json'); + await bundle.build( + packageConfigPath: '.dart_tool/package_config.json', + targetPlatform: TargetPlatform.tester, + ); expect( bundle.entries.keys, unorderedEquals(['NOTICES.Z', 'AssetManifest.bin', 'FontManifest.json']), @@ -665,7 +676,10 @@ $assetsSection writeAssets('p/p/', assetsOnDisk); final AssetBundle bundle = AssetBundleFactory.instance.createBundle(); - await bundle.build(packageConfigPath: '.dart_tool/package_config.json'); + await bundle.build( + packageConfigPath: '.dart_tool/package_config.json', + targetPlatform: TargetPlatform.tester, + ); expect( bundle.entries['AssetManifest.bin'], @@ -754,7 +768,10 @@ $assetsSection writePubspecFile('p/p/pubspec.yaml', 'test_package', assets: assetOnManifest); final AssetBundle bundle = AssetBundleFactory.instance.createBundle(); - await bundle.build(packageConfigPath: '.dart_tool/package_config.json'); + await bundle.build( + packageConfigPath: '.dart_tool/package_config.json', + targetPlatform: TargetPlatform.tester, + ); }, overrides: { FileSystem: () => testFileSystem, diff --git a/packages/flutter_tools/test/general.shard/asset_bundle_test.dart b/packages/flutter_tools/test/general.shard/asset_bundle_test.dart index fcb8031c4be..3ea4e5b4c53 100644 --- a/packages/flutter_tools/test/general.shard/asset_bundle_test.dart +++ b/packages/flutter_tools/test/general.shard/asset_bundle_test.dart @@ -44,7 +44,13 @@ void main() { 'nonempty', () async { final AssetBundle ab = AssetBundleFactory.instance.createBundle(); - expect(await ab.build(packageConfigPath: '.dart_tool/package_config.json'), 0); + expect( + await ab.build( + packageConfigPath: '.dart_tool/package_config.json', + targetPlatform: TargetPlatform.tester, + ), + 0, + ); expect(ab.entries.length, greaterThan(0)); }, overrides: { @@ -62,7 +68,10 @@ void main() { ..writeAsStringSync(''); final AssetBundle bundle = AssetBundleFactory.instance.createBundle(); - await bundle.build(packageConfigPath: '.dart_tool/package_config.json'); + await bundle.build( + packageConfigPath: '.dart_tool/package_config.json', + targetPlatform: TargetPlatform.tester, + ); expect(bundle.entries.keys, unorderedEquals(['AssetManifest.bin'])); const expectedBinAssetManifest = {}; expect( @@ -109,7 +118,10 @@ flutter: } final AssetBundle bundle = AssetBundleFactory.instance.createBundle(); - await bundle.build(packageConfigPath: '.dart_tool/package_config.json'); + await bundle.build( + packageConfigPath: '.dart_tool/package_config.json', + targetPlatform: TargetPlatform.tester, + ); expect( bundle.entries.keys, @@ -149,7 +161,10 @@ flutter: - assets/foo/ '''); final AssetBundle bundle = AssetBundleFactory.instance.createBundle(); - await bundle.build(packageConfigPath: '.dart_tool/package_config.json'); + await bundle.build( + packageConfigPath: '.dart_tool/package_config.json', + targetPlatform: TargetPlatform.tester, + ); expect( bundle.entries.keys, unorderedEquals([ @@ -165,7 +180,10 @@ flutter: ..setLastModifiedSync(packageFile.lastModifiedSync().add(const Duration(hours: 1))); expect(bundle.needsBuild(), true); - await bundle.build(packageConfigPath: '.dart_tool/package_config.json'); + await bundle.build( + packageConfigPath: '.dart_tool/package_config.json', + targetPlatform: TargetPlatform.tester, + ); expect( bundle.entries.keys, unorderedEquals([ @@ -203,7 +221,10 @@ flutter: mainLibName: 'my_app', ); final AssetBundle bundle = AssetBundleFactory.instance.createBundle(); - await bundle.build(packageConfigPath: '.dart_tool/package_config.json'); + await bundle.build( + packageConfigPath: '.dart_tool/package_config.json', + targetPlatform: TargetPlatform.tester, + ); expect( bundle.entries.keys, unorderedEquals([ @@ -231,7 +252,10 @@ name: my_app''') // asset manifest and not updated. This is due to the devfs not // supporting file deletion. expect(bundle.needsBuild(), true); - await bundle.build(packageConfigPath: '.dart_tool/package_config.json'); + await bundle.build( + packageConfigPath: '.dart_tool/package_config.json', + targetPlatform: TargetPlatform.tester, + ); expect( bundle.entries.keys, unorderedEquals([ @@ -269,7 +293,10 @@ flutter: '''); writePackageConfigFiles(directory: globals.fs.currentDirectory, mainLibName: 'my_app'); final AssetBundle bundle = AssetBundleFactory.instance.createBundle(); - await bundle.build(packageConfigPath: '.dart_tool/package_config.json'); + await bundle.build( + packageConfigPath: '.dart_tool/package_config.json', + targetPlatform: TargetPlatform.tester, + ); expect( bundle.entries.keys, unorderedEquals([ @@ -323,6 +350,7 @@ flutter: await bundle.build( packageConfigPath: '.dart_tool/package_config.json', deferredComponentsEnabled: true, + targetPlatform: TargetPlatform.tester, ); expect( bundle.entries.keys, @@ -371,7 +399,10 @@ flutter: - assets/wild/ '''); final AssetBundle bundle = AssetBundleFactory.instance.createBundle(); - await bundle.build(packageConfigPath: '.dart_tool/package_config.json'); + await bundle.build( + packageConfigPath: '.dart_tool/package_config.json', + targetPlatform: TargetPlatform.tester, + ); expect( bundle.entries.keys, unorderedEquals([ @@ -431,6 +462,7 @@ flutter: await bundle.build( packageConfigPath: '.dart_tool/package_config.json', deferredComponentsEnabled: true, + targetPlatform: TargetPlatform.tester, ); expect( bundle.entries.keys, @@ -454,6 +486,7 @@ flutter: await bundle.build( packageConfigPath: '.dart_tool/package_config.json', deferredComponentsEnabled: true, + targetPlatform: TargetPlatform.tester, ); expect( @@ -507,6 +540,7 @@ flutter: () => bundle.build( packageConfigPath: '.dart_tool/package_config.json', flutterProject: FlutterProject.fromDirectoryTest(fileSystem.currentDirectory), + targetPlatform: TargetPlatform.tester, ), throwsToolExit( message: @@ -546,6 +580,7 @@ flutter: () => bundle.build( packageConfigPath: '.dart_tool/package_config.json', flutterProject: FlutterProject.fromDirectoryTest(fileSystem.currentDirectory), + targetPlatform: TargetPlatform.tester, ), throwsToolExit( message: @@ -590,6 +625,7 @@ flutter: await bundle.build( packageConfigPath: '.dart_tool/package_config.json', flutterProject: FlutterProject.fromDirectoryTest(fileSystem.currentDirectory), + targetPlatform: TargetPlatform.tester, ); expect(bundle.entries['my-asset.txt']!.content.isModified, isTrue); @@ -597,6 +633,7 @@ flutter: await bundle.build( packageConfigPath: '.dart_tool/package_config.json', flutterProject: FlutterProject.fromDirectoryTest(fileSystem.currentDirectory), + targetPlatform: TargetPlatform.tester, ); expect(bundle.entries['my-asset.txt']!.content.isModified, isFalse); @@ -614,6 +651,7 @@ flutter: await bundle.build( packageConfigPath: '.dart_tool/package_config.json', flutterProject: FlutterProject.fromDirectoryTest(fileSystem.currentDirectory), + targetPlatform: TargetPlatform.tester, ); expect(bundle.entries['my-asset.txt']!.content.isModified, isTrue); @@ -762,12 +800,18 @@ assets: - assets/foo/bar.txt '''); final AssetBundle bundle = AssetBundleFactory.instance.createBundle(); - await bundle.build(packageConfigPath: '.dart_tool/package_config.json'); + await bundle.build( + packageConfigPath: '.dart_tool/package_config.json', + targetPlatform: TargetPlatform.tester, + ); final AssetBundleEntry? fontManifest = bundle.entries['FontManifest.json']; final AssetBundleEntry? license = bundle.entries['NOTICES']; - await bundle.build(packageConfigPath: '.dart_tool/package_config.json'); + await bundle.build( + packageConfigPath: '.dart_tool/package_config.json', + targetPlatform: TargetPlatform.tester, + ); expect(fontManifest, bundle.entries['FontManifest.json']); expect(license, bundle.entries['NOTICES']); @@ -795,7 +839,13 @@ flutter: '''); final AssetBundle bundle = AssetBundleFactory.instance.createBundle(); - expect(await bundle.build(packageConfigPath: '.dart_tool/package_config.json'), 0); + expect( + await bundle.build( + packageConfigPath: '.dart_tool/package_config.json', + targetPlatform: TargetPlatform.tester, + ), + 0, + ); expect( bundle.additionalDependencies.single.path, contains('DOES_NOT_EXIST_RERUN_FOR_WILDCARD'), @@ -824,7 +874,13 @@ flutter: '''); final AssetBundle bundle = AssetBundleFactory.instance.createBundle(); - expect(await bundle.build(packageConfigPath: '.dart_tool/package_config.json'), 0); + expect( + await bundle.build( + packageConfigPath: '.dart_tool/package_config.json', + targetPlatform: TargetPlatform.tester, + ), + 0, + ); expect(bundle.additionalDependencies, isEmpty); }, overrides: { @@ -871,7 +927,13 @@ flutter: '''); final AssetBundle bundle = AssetBundleFactory.instance.createBundle(); - expect(await bundle.build(packageConfigPath: '.dart_tool/package_config.json'), 0); + expect( + await bundle.build( + packageConfigPath: '.dart_tool/package_config.json', + targetPlatform: TargetPlatform.tester, + ), + 0, + ); await writeBundle( output, @@ -1097,7 +1159,13 @@ flutter: final AssetBundle bundle = AssetBundleFactory.instance.createBundle(); globals.fs.file('foo/bar/fizz.txt').createSync(recursive: true); - expect(await bundle.build(packageConfigPath: '.dart_tool/package_config.json'), 0); + expect( + await bundle.build( + packageConfigPath: '.dart_tool/package_config.json', + targetPlatform: TargetPlatform.tester, + ), + 0, + ); expect(bundle.additionalDependencies, isEmpty); }, overrides: { @@ -1135,7 +1203,10 @@ flutter: final AssetBundle bundle = AssetBundleFactory.instance.createBundle(); globals.fs.file('foo/bar/fizz.txt').createSync(recursive: true); - await bundle.build(packageConfigPath: '.dart_tool/package_config.json'); + await bundle.build( + packageConfigPath: '.dart_tool/package_config.json', + targetPlatform: TargetPlatform.tester, + ); expect( bundle.entries.keys, @@ -1188,7 +1259,13 @@ flutter: '''); final AssetBundle bundle = AssetBundleFactory.instance.createBundle(); - expect(await bundle.build(packageConfigPath: '.dart_tool/package_config.json'), 1); + expect( + await bundle.build( + packageConfigPath: '.dart_tool/package_config.json', + targetPlatform: TargetPlatform.tester, + ), + 1, + ); expect(testLogger.errorText, contains('This asset was included from package foo')); }, overrides: { @@ -1217,7 +1294,13 @@ flutter: '''); final AssetBundle bundle = AssetBundleFactory.instance.createBundle(); - expect(await bundle.build(packageConfigPath: '.dart_tool/package_config.json'), 1); + expect( + await bundle.build( + packageConfigPath: '.dart_tool/package_config.json', + targetPlatform: TargetPlatform.tester, + ), + 1, + ); expect(testLogger.errorText, isNot(contains('This asset was included from'))); }, overrides: { @@ -1256,7 +1339,13 @@ flutter: '''); final AssetBundle bundle = AssetBundleFactory.instance.createBundle(); - expect(await bundle.build(packageConfigPath: '.dart_tool/package_config.json'), 0); + expect( + await bundle.build( + packageConfigPath: '.dart_tool/package_config.json', + targetPlatform: TargetPlatform.tester, + ), + 0, + ); expect((bundle.entries['FontManifest.json']!.content as DevFSStringContent).string, '[]'); expect(testLogger.errorText, contains('package:foo has `uses-material-design: true` set')); }, @@ -1292,7 +1381,13 @@ flutter: final AssetBundle bundle = AssetBundleFactory.instance.createBundle(); - expect(await bundle.build(packageConfigPath: '.dart_tool/package_config.json'), 0); + expect( + await bundle.build( + packageConfigPath: '.dart_tool/package_config.json', + targetPlatform: TargetPlatform.tester, + ), + 0, + ); expect( bundle.entries.keys, unorderedEquals([ @@ -1336,7 +1431,13 @@ flutter: globals.fs.file('assets/zebra.jpg').createSync(); final AssetBundle bundle = AssetBundleFactory.instance.createBundle(); - expect(await bundle.build(packageConfigPath: '.dart_tool/package_config.json'), 0); + expect( + await bundle.build( + packageConfigPath: '.dart_tool/package_config.json', + targetPlatform: TargetPlatform.tester, + ), + 0, + ); expect((bundle.entries['FontManifest.json']!.content as DevFSStringContent).string, '[]'); }, overrides: { @@ -1375,6 +1476,7 @@ flutter: () => bundle.build( packageConfigPath: '.dart_tool/package_config.json', flutterProject: FlutterProject.fromDirectoryTest(fileSystem.currentDirectory), + targetPlatform: TargetPlatform.tester, ), throwsToolExit( message: diff --git a/packages/flutter_tools/test/general.shard/asset_bundle_variant_test.dart b/packages/flutter_tools/test/general.shard/asset_bundle_variant_test.dart index af682ba587e..91f24c632dc 100644 --- a/packages/flutter_tools/test/general.shard/asset_bundle_variant_test.dart +++ b/packages/flutter_tools/test/general.shard/asset_bundle_variant_test.dart @@ -12,6 +12,7 @@ import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/platform.dart'; import 'package:flutter_tools/src/base/user_messages.dart'; +import 'package:flutter_tools/src/build_info.dart'; import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/project.dart'; @@ -86,6 +87,7 @@ ${assets.map((String entry) => ' - $entry').join('\n')} await bundle.build( packageConfigPath: '.dart_tool/package_config.json', flutterProject: FlutterProject.fromDirectoryTest(fs.currentDirectory), + targetPlatform: TargetPlatform.tester, ); final Map smcBinManifest = await extractAssetManifestSmcBinFromBundle( @@ -133,6 +135,7 @@ ${assets.map((String entry) => ' - $entry').join('\n')} await bundle.build( packageConfigPath: '.dart_tool/package_config.json', flutterProject: FlutterProject.fromDirectoryTest(fs.currentDirectory), + targetPlatform: TargetPlatform.tester, ); final Map smcBinManifest = await extractAssetManifestSmcBinFromBundle( @@ -176,6 +179,7 @@ ${assets.map((String entry) => ' - $entry').join('\n')} await bundle.build( packageConfigPath: '.dart_tool/package_config.json', flutterProject: FlutterProject.fromDirectoryTest(fs.currentDirectory), + targetPlatform: TargetPlatform.tester, ); final Map smcBinManifest = await extractAssetManifestSmcBinFromBundle( @@ -215,6 +219,7 @@ ${assets.map((String entry) => ' - $entry').join('\n')} await bundle.build( packageConfigPath: '.dart_tool/package_config.json', flutterProject: FlutterProject.fromDirectoryTest(fs.currentDirectory), + targetPlatform: TargetPlatform.tester, ); final expectedManifest = >>{ @@ -282,6 +287,7 @@ flutter: await bundle.build( packageConfigPath: '.dart_tool/package_config.json', flutterProject: FlutterProject.fromDirectoryTest(fs.currentDirectory), + targetPlatform: TargetPlatform.tester, ); final expectedAssetManifest = >>{ diff --git a/packages/flutter_tools/test/general.shard/asset_test.dart b/packages/flutter_tools/test/general.shard/asset_test.dart index 5659c430f50..0f324853abd 100644 --- a/packages/flutter_tools/test/general.shard/asset_test.dart +++ b/packages/flutter_tools/test/general.shard/asset_test.dart @@ -96,6 +96,7 @@ dependencies: packageConfigPath: packageConfigPath, manifestPath: manifestPath, flutterProject: FlutterProject.fromDirectoryTest(fileSystem.directory('main')), + targetPlatform: TargetPlatform.tester, ); expect(assetBundle.entries, contains('FontManifest.json')); @@ -254,6 +255,7 @@ flutter: packageConfigPath: packageConfigPath, manifestPath: manifestPath, flutterProject: FlutterProject.fromDirectoryTest(fileSystem.directory('main')), + targetPlatform: TargetPlatform.tester, ); expect(assetBundle.entries, contains('FontManifest.json')); @@ -298,6 +300,7 @@ flutter: manifestPath: manifestPath, // file doesn't exist packageConfigPath: packageConfigPath, flutterProject: FlutterProject.fromDirectoryTest(fileSystem.file(manifestPath).parent), + targetPlatform: TargetPlatform.tester, ); expect(assetBundle.wasBuiltOnce(), true); @@ -397,6 +400,7 @@ flutter: final int result = await assetBundle.build( packageConfigPath: '.dart_tool/package_config.json', flutterProject: FlutterProject.fromDirectoryTest(fileSystem.currentDirectory), + targetPlatform: TargetPlatform.tester, ); expect(result, isNot(0)); expect( @@ -426,6 +430,7 @@ flutter: final int result = await assetBundle.build( packageConfigPath: '.dart_tool/package_config.json', flutterProject: FlutterProject.fromDirectoryTest(fileSystem.currentDirectory), + targetPlatform: TargetPlatform.tester, ); expect(result, isNot(0)); expect( diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/assets_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/assets_test.dart index b8b22a81eef..cf55efed9e3 100644 --- a/packages/flutter_tools/test/general.shard/build_system/targets/assets_test.dart +++ b/packages/flutter_tools/test/general.shard/build_system/targets/assets_test.dart @@ -461,6 +461,161 @@ flutter: }, ); + group('platform-specific assets', () { + /// All supported platforms that should be validated. + const kValidPluginPlatforms = {'android', 'ios', 'web', 'windows', 'linux', 'macos'}; + + TargetPlatform targetFor(String platform) => + TargetPlatform.values.firstWhere((p) => p.osName == platform); + + /// Writes a `pubspec.yaml` with an asset, optionally restricted to + /// certain [platforms], then runs the build for [targetPlatform] and + /// returns whether the asset was bundled. + /// + /// This helper reflects how Flutter decides which assets to include + /// depending on the `platforms:` key in `pubspec.yaml`. + Future setupAndBuildPlatformAsset(String platform, TargetPlatform targetPlatform) async { + final filePath = 'assets/test-$platform.txt'; + + final pubspec = platform.isEmpty + ? ''' +name: example +flutter: + assets: + - path: $filePath +''' + : ''' +name: example +flutter: + assets: + - path: $filePath + platforms: + - $platform +'''; + + fileSystem.file('pubspec.yaml') + ..createSync() + ..writeAsStringSync(pubspec); + writePackageConfigFiles(directory: globals.fs.currentDirectory, mainLibName: 'example'); + fileSystem.file(filePath).createSync(recursive: true); + + await const CopyAssets().build(environment, targetPlatform: targetPlatform); + + final File file = fileSystem.file('${environment.buildDir.path}/flutter_assets/$filePath'); + return file.existsSync(); + } + + group('includes assets only for matching platform', () { + for (final platform in kValidPluginPlatforms) { + testUsingContext( + platform, + () async { + final TargetPlatform targetPlatform = targetFor(platform); + final bool didInclude = await setupAndBuildPlatformAsset(platform, targetPlatform); + + expect(didInclude, isTrue, reason: 'Expected asset for $platform to be included'); + }, + overrides: { + FileSystem: () => fileSystem, + ProcessManager: () => FakeProcessManager.any(), + }, + ); + } + }); + + group('skips assets for non-matching platform', () { + for (final platform in kValidPluginPlatforms) { + testUsingContext( + platform, + () async { + final TargetPlatform targetPlatform = platform == 'android' + ? TargetPlatform.ios + : TargetPlatform.android; + final bool didInclude = await setupAndBuildPlatformAsset(platform, targetPlatform); + + expect( + didInclude, + isFalse, + reason: 'Expected asset for $platform to be skipped when target is $targetPlatform', + ); + }, + overrides: { + FileSystem: () => fileSystem, + ProcessManager: () => FakeProcessManager.any(), + }, + ); + } + }); + + group('includes assets for all platforms when no restriction is set', () { + for (final platform in kValidPluginPlatforms) { + testUsingContext( + platform, + () async { + final TargetPlatform targetPlatform = targetFor(platform); + final bool didInclude = await setupAndBuildPlatformAsset('', targetPlatform); + + expect( + didInclude, + isTrue, + reason: + 'Expected asset to be included for all platforms when no platforms are specified (platform: $platform)', + ); + }, + overrides: { + FileSystem: () => fileSystem, + ProcessManager: () => FakeProcessManager.any(), + }, + ); + } + }); + + group('includes assets only for declared multiple platforms', () { + for (final platform in kValidPluginPlatforms) { + testUsingContext( + platform, + () async { + const filePath = 'assets/test-multi.txt'; + final targetPlatforms = ['android', 'ios']; + + fileSystem.file('pubspec.yaml') + ..createSync() + ..writeAsStringSync(''' +name: example +flutter: + assets: + - path: $filePath + platforms: [${targetPlatforms.join(',')}] +'''); + + writePackageConfigFiles(directory: globals.fs.currentDirectory, mainLibName: 'example'); + fileSystem.file(filePath).createSync(recursive: true); + + final TargetPlatform targetPlatform = targetFor(platform); + + await const CopyAssets().build(environment, targetPlatform: targetPlatform); + + final File bundledFile = fileSystem.file( + '${environment.buildDir.path}/flutter_assets/$filePath', + ); + + final bool exists = bundledFile.existsSync(); + + if (targetPlatforms.contains(platform)) { + expect(exists, isTrue, reason: 'Expected asset to be included for $platform'); + } else { + expect(exists, isFalse, reason: 'Expected asset to be skipped for $platform'); + } + }, + overrides: { + FileSystem: () => fileSystem, + ProcessManager: () => FakeProcessManager.any(), + }, + ); + } + }); + }); + testUsingContext( 'Uses processors~/2 to transform assets', () async { diff --git a/packages/flutter_tools/test/general.shard/flutter_manifest_test.dart b/packages/flutter_tools/test/general.shard/flutter_manifest_test.dart index 1b98f149bb7..3e39438e0ac 100644 --- a/packages/flutter_tools/test/general.shard/flutter_manifest_test.dart +++ b/packages/flutter_tools/test/general.shard/flutter_manifest_test.dart @@ -1516,6 +1516,74 @@ flutter: expect(logger.errorText, 'Expected "default-flavor" to be a string, but got 3 (int).\n'); }); + testWithoutContext('FlutterManifest parses asset with platforms', () async { + const manifest = ''' +name: test +dependencies: + flutter: + sdk: flutter +flutter: + assets: + - path: assets/test.png + platforms: + - web + - android +'''; + + final FlutterManifest flutterManifest = FlutterManifest.createFromString( + manifest, + logger: logger, + )!; + + expect(flutterManifest.assets, hasLength(1)); + final AssetsEntry entry = flutterManifest.assets.single; + expect(entry.uri.path, 'assets/test.png'); + expect(entry.platforms, containsAll(['web', 'android'])); + }); + + testWithoutContext( + 'FlutterManifest fails when platforms contains invalid platform name', + () async { + const manifest = ''' +name: test +flutter: + assets: + - path: assets/test.png + platforms: + - toasterOS + - windows +'''; + + final FlutterManifest? flutterManifest = FlutterManifest.createFromString( + manifest, + logger: logger, + ); + + expect(flutterManifest, isNull); + expect(logger.errorText, contains('Invalid platform')); + }, + ); + + testWithoutContext('FlutterManifest supports empty platforms list', () async { + const manifest = ''' +name: test +flutter: + assets: + - path: assets/test.png + platforms: [] +'''; + + final FlutterManifest flutterManifest = FlutterManifest.createFromString( + manifest, + logger: logger, + )!; + + expect(flutterManifest.assets, hasLength(1)); + final AssetsEntry entry = flutterManifest.assets.single; + expect(entry.uri.path, 'assets/test.png'); + expect(entry.platforms, isEmpty); + }); + testWithoutContext('FlutterManifest.copyWith generates a valid manifest', () async { const manifest = ''' name: test @@ -1536,6 +1604,7 @@ flutter: AssetsEntry( uri: Uri(path: 'foo'), flavors: const {'flavor'}, + platforms: const {'web', 'android', 'ios'}, transformers: const [ AssetTransformerEntry(package: 'package:foo', args: ['arg']), ], @@ -1555,6 +1624,7 @@ flutter: AssetsEntry( uri: Uri(path: 'deferredComponentUri'), flavors: const {'deferredComponentFlavor'}, + platforms: const {'macos'}, transformers: const [ AssetTransformerEntry( package: 'package:deferredComponent', @@ -1578,6 +1648,10 @@ flutter: - path: foo flavors: - flavor + platforms: + - web + - android + - ios transformers: - package: package:foo args: @@ -1598,6 +1672,8 @@ flutter: - path: deferredComponentUri flavors: - deferredComponentFlavor + platforms: + - macos transformers: - package: package:deferredComponent args: