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: