diff --git a/packages/flutter_tools/lib/src/asset.dart b/packages/flutter_tools/lib/src/asset.dart index b8b35ac69dc..2f12d160ce9 100644 --- a/packages/flutter_tools/lib/src/asset.dart +++ b/packages/flutter_tools/lib/src/asset.dart @@ -958,8 +958,8 @@ class ManifestAssetBundle implements AssetBundle { } on UnsupportedError catch (e) { throwToolExit( 'Unable to search for asset files in directory path "${assetUri.path}". ' - 'Please ensure that this is valid URI that points to a directory ' - 'that is available on the local file system.\nError details:\n$e'); + 'Please ensure that this entry in pubspec.yaml is a valid file path.\n' + 'Error details:\n$e'); } if (!_fileSystem.directory(directoryPath).existsSync()) { @@ -1296,17 +1296,25 @@ class _AssetDirectoryCache { final Map> _variantsPerFolder = >{}; List variantsFor(String assetPath) { - final String directory = _fileSystem.path.dirname(assetPath); + final String directoryName = _fileSystem.path.dirname(assetPath); - if (!_fileSystem.directory(directory).existsSync()) { - return const []; + try { + if (!_fileSystem.directory(directoryName).existsSync()) { + return const []; + } + } on FileSystemException catch (e) { + throwToolExit( + 'Unable to check the existence of asset file "$assetPath". ' + 'Ensure that the asset file is declared as a valid local file system path.\n' + 'Details: $e', + ); } if (_cache.containsKey(assetPath)) { return _cache[assetPath]!; } - if (!_variantsPerFolder.containsKey(directory)) { - _variantsPerFolder[directory] = _fileSystem.directory(directory) + if (!_variantsPerFolder.containsKey(directoryName)) { + _variantsPerFolder[directoryName] = _fileSystem.directory(directoryName) .listSync() .whereType() .where((Directory dir) => _assetVariantDirectoryRegExp.hasMatch(dir.basename)) @@ -1315,7 +1323,7 @@ class _AssetDirectoryCache { .toList(); } final File assetFile = _fileSystem.file(assetPath); - final List potentialVariants = _variantsPerFolder[directory]!; + final List potentialVariants = _variantsPerFolder[directoryName]!; final String basename = assetFile.basename; return _cache[assetPath] = [ // It's possible that the user specifies only explicit variants (e.g. .../1x/asset.png), 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 new file mode 100644 index 00000000000..9a5eeb86977 --- /dev/null +++ b/packages/flutter_tools/test/general.shard/asset_bundle_flavors_test.dart @@ -0,0 +1,249 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:file/memory.dart'; +import 'package:flutter_tools/src/asset.dart'; +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/cache.dart'; +import 'package:flutter_tools/src/project.dart'; + +import '../src/common.dart'; + +void main() { + Future buildBundleWithFlavor(String? flavor, { + required Logger logger, + required FileSystem fileSystem, + required Platform platform, + }) async { + final ManifestAssetBundle bundle = ManifestAssetBundle( + logger: logger, + fileSystem: fileSystem, + platform: platform, + flutterRoot: Cache.defaultFlutterRoot( + platform: platform, + fileSystem: fileSystem, + userMessages: UserMessages(), + ), + splitDeferredAssets: true, + ); + + await bundle.build( + packagesPath: '.packages', + flutterProject: FlutterProject.fromDirectoryTest(fileSystem.currentDirectory), + flavor: flavor, + ); + return bundle; + } + + testWithoutContext('correctly bundles assets given a simple asset manifest with flavors', () async { + final MemoryFileSystem fileSystem = MemoryFileSystem(); + fileSystem.currentDirectory = fileSystem.systemTempDirectory.createTempSync('flutter_asset_bundle_test.'); + final BufferLogger logger = BufferLogger.test(); + final FakePlatform platform = FakePlatform(); + + fileSystem.file('.packages').createSync(); + fileSystem.file(fileSystem.path.join('assets', 'common', 'image.png')).createSync(recursive: true); + fileSystem.file(fileSystem.path.join('assets', 'vanilla', 'ice-cream.png')).createSync(recursive: true); + fileSystem.file(fileSystem.path.join('assets', 'strawberry', 'ice-cream.png')).createSync(recursive: true); + fileSystem.file(fileSystem.path.join('assets', 'orange', 'ice-cream.png')).createSync(recursive: true); + fileSystem.file('pubspec.yaml') + ..createSync() + ..writeAsStringSync(r''' +name: example +flutter: + assets: + - assets/common/ + - path: assets/vanilla/ + flavors: + - vanilla + - path: assets/strawberry/ + flavors: + - strawberry + - path: assets/orange/ice-cream.png + flavors: + - orange +'''); + + ManifestAssetBundle bundle; + bundle = await buildBundleWithFlavor( + null, + logger: logger, + fileSystem: fileSystem, + platform: platform, + ); + expect(bundle.entries.keys, contains('assets/common/image.png')); + expect(bundle.entries.keys, isNot(contains('assets/vanilla/ice-cream.png'))); + expect(bundle.entries.keys, isNot(contains('assets/strawberry/ice-cream.png'))); + expect(bundle.entries.keys, isNot(contains('assets/orange/ice-cream.png'))); + + bundle = await buildBundleWithFlavor( + 'strawberry', + logger: logger, + fileSystem: fileSystem, + platform: platform, + ); + expect(bundle.entries.keys, contains('assets/common/image.png')); + expect(bundle.entries.keys, isNot(contains('assets/vanilla/ice-cream.png'))); + expect(bundle.entries.keys, contains('assets/strawberry/ice-cream.png')); + expect(bundle.entries.keys, isNot(contains('assets/orange/ice-cream.png'))); + + bundle = await buildBundleWithFlavor( + 'orange', + logger: logger, + fileSystem: fileSystem, + platform: platform, + ); + expect(bundle.entries.keys, contains('assets/common/image.png')); + expect(bundle.entries.keys, isNot(contains('assets/vanilla/ice-cream.png'))); + expect(bundle.entries.keys, isNot(contains('assets/strawberry/ice-cream.png'))); + expect(bundle.entries.keys, contains('assets/orange/ice-cream.png')); + }); + + testWithoutContext('throws a tool exit when a non-flavored folder contains a flavored asset', () async { + final MemoryFileSystem fileSystem = MemoryFileSystem(); + fileSystem.currentDirectory = fileSystem.systemTempDirectory.createTempSync('flutter_asset_bundle_test.'); + final BufferLogger logger = BufferLogger.test(); + final FakePlatform platform = FakePlatform(); + fileSystem.file('.packages').createSync(); + fileSystem.file(fileSystem.path.join('assets', 'unflavored.png')).createSync(recursive: true); + fileSystem.file(fileSystem.path.join('assets', 'vanillaOrange.png')).createSync(recursive: true); + + fileSystem.file('pubspec.yaml') + ..createSync() + ..writeAsStringSync(r''' +name: example +flutter: + assets: + - assets/ + - path: assets/vanillaOrange.png + flavors: + - vanilla + - orange +'''); + + expect( + buildBundleWithFlavor( + null, + logger: logger, + fileSystem: fileSystem, + platform: platform, + ), + throwsToolExit(message: 'Multiple assets entries include the file ' + '"assets/vanillaOrange.png", but they specify different lists of flavors.\n' + 'An entry with the path "assets/" does not specify any flavors.\n' + 'An entry with the path "assets/vanillaOrange.png" specifies the flavor(s): "vanilla", "orange".\n\n' + 'Consider organizing assets with different flavors into different directories.'), + ); + }); + + testWithoutContext('throws a tool exit when a flavored folder contains a flavorless asset', () async { + final MemoryFileSystem fileSystem = MemoryFileSystem(); + fileSystem.currentDirectory = fileSystem.systemTempDirectory.createTempSync('flutter_asset_bundle_test.'); + final BufferLogger logger = BufferLogger.test(); + final FakePlatform platform = FakePlatform(); + fileSystem.file('.packages').createSync(); + fileSystem.file(fileSystem.path.join('vanilla', 'vanilla.png')).createSync(recursive: true); + fileSystem.file(fileSystem.path.join('vanilla', 'flavorless.png')).createSync(recursive: true); + + fileSystem.file('pubspec.yaml') + ..createSync() + ..writeAsStringSync(r''' +name: example +flutter: + assets: + - path: vanilla/ + flavors: + - vanilla + - vanilla/flavorless.png +'''); + expect( + buildBundleWithFlavor( + null, + logger: logger, + fileSystem: fileSystem, + platform: platform, + ), + throwsToolExit(message: 'Multiple assets entries include the file ' + '"vanilla/flavorless.png", but they specify different lists of flavors.\n' + 'An entry with the path "vanilla/" specifies the flavor(s): "vanilla".\n' + 'An entry with the path "vanilla/flavorless.png" does not specify any flavors.\n\n' + 'Consider organizing assets with different flavors into different directories.'), + ); + }); + + testWithoutContext('tool exits when two file-explicit entries give the same asset different flavors', () { + final MemoryFileSystem fileSystem = MemoryFileSystem(); + fileSystem.currentDirectory = fileSystem.systemTempDirectory.createTempSync('flutter_asset_bundle_test.'); + final BufferLogger logger = BufferLogger.test(); + final FakePlatform platform = FakePlatform(); + fileSystem.file('.packages').createSync(); + fileSystem.file('orange.png').createSync(recursive: true); + fileSystem.file('pubspec.yaml') + ..createSync() + ..writeAsStringSync(r''' +name: example +flutter: + assets: + - path: orange.png + flavors: + - orange + - path: orange.png + flavors: + - mango +'''); + + expect( + buildBundleWithFlavor( + null, + logger: logger, + fileSystem: fileSystem, + platform: platform, + ), + throwsToolExit(message: 'Multiple assets entries include the file ' + '"orange.png", but they specify different lists of flavors.\n' + 'An entry with the path "orange.png" specifies the flavor(s): "orange".\n' + 'An entry with the path "orange.png" specifies the flavor(s): "mango".'), + ); +}); + + testWithoutContext('throws ToolExit when flavor from file-level declaration has different flavor from containing folder flavor declaration', () async { + final MemoryFileSystem fileSystem = MemoryFileSystem(); + fileSystem.currentDirectory = fileSystem.systemTempDirectory.createTempSync('flutter_asset_bundle_test.'); + final BufferLogger logger = BufferLogger.test(); + final FakePlatform platform = FakePlatform(); + fileSystem.file('.packages').createSync(); + fileSystem.file(fileSystem.path.join('vanilla', 'actually-strawberry.png')).createSync(recursive: true); + fileSystem.file(fileSystem.path.join('vanilla', 'vanilla.png')).createSync(recursive: true); + + fileSystem.file('pubspec.yaml') + ..createSync() + ..writeAsStringSync(r''' +name: example +flutter: + assets: + - path: vanilla/ + flavors: + - vanilla + - path: vanilla/actually-strawberry.png + flavors: + - strawberry +'''); + expect( + buildBundleWithFlavor( + null, + logger: logger, + fileSystem: fileSystem, + platform: platform, + ), + throwsToolExit(message: 'Multiple assets entries include the file ' + '"vanilla/actually-strawberry.png", but they specify different lists of flavors.\n' + 'An entry with the path "vanilla/" specifies the flavor(s): "vanilla".\n' + 'An entry with the path "vanilla/actually-strawberry.png" ' + 'specifies the flavor(s): "strawberry".'), + ); + }); +} 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 81c9fc30023..3ea89592736 100644 --- a/packages/flutter_tools/test/general.shard/asset_bundle_test.dart +++ b/packages/flutter_tools/test/general.shard/asset_bundle_test.dart @@ -24,18 +24,16 @@ import 'package:standard_message_codec/standard_message_codec.dart'; import '../src/common.dart'; import '../src/context.dart'; -const String shaderLibDir = '/./shader_lib'; - void main() { - group('AssetBundle.build', () { - late Logger logger; + const String shaderLibDir = '/./shader_lib'; + + group('AssetBundle.build (using context)', () { late FileSystem testFileSystem; late Platform platform; setUp(() async { testFileSystem = MemoryFileSystem(); testFileSystem.currentDirectory = testFileSystem.systemTempDirectory.createTempSync('flutter_asset_bundle_test.'); - logger = BufferLogger.test(); platform = FakePlatform(); }); @@ -153,30 +151,6 @@ flutter: ProcessManager: () => FakeProcessManager.any(), }); - testUsingContext('throws ToolExit when directory entry contains invalid characters', () async { - testFileSystem.file('.packages').createSync(); - testFileSystem.file('pubspec.yaml') - ..createSync() - ..writeAsStringSync(r''' -name: example -flutter: - assets: - - https://mywebsite.com/images/ -'''); - final AssetBundle bundle = AssetBundleFactory.instance.createBundle(); - expect(() => bundle.build(packagesPath: '.packages'), throwsToolExit( - message: 'Unable to search for asset files in directory path "https%3A//mywebsite.com/images/". ' - 'Please ensure that this is valid URI that points to a directory that is ' - 'available on the local file system.\n' - 'Error details:\n' - 'Unsupported operation: Illegal character in path: https:', - )); - }, overrides: { - FileSystem: () => testFileSystem, - ProcessManager: () => FakeProcessManager.any(), - Platform: () => FakePlatform(operatingSystem: 'windows'), - }); - testUsingContext('handle removal of wildcard directories', () async { globals.fs.file(globals.fs.path.join('assets', 'foo', 'bar.txt')).createSync(recursive: true); final File pubspec = globals.fs.file('pubspec.yaml') @@ -361,180 +335,98 @@ flutter: Platform: () => platform, ProcessManager: () => FakeProcessManager.any(), }); + }); - group('flavors feature', () { - Future buildBundleWithFlavor(String? flavor) async { - final ManifestAssetBundle bundle = ManifestAssetBundle( - logger: logger, - fileSystem: testFileSystem, - platform: platform, - flutterRoot: Cache.defaultFlutterRoot( - platform: platform, - fileSystem: testFileSystem, - userMessages: UserMessages(), - ), - splitDeferredAssets: true, - ); + group('AssetBundle.build', () { + testWithoutContext('throws ToolExit when directory entry contains invalid characters (Windows only)', () async { + final MemoryFileSystem fileSystem = MemoryFileSystem(style: FileSystemStyle.windows); + final BufferLogger logger = BufferLogger.test(); + final FakePlatform platform = FakePlatform(operatingSystem: 'windows'); + final String flutterRoot = Cache.defaultFlutterRoot( + platform: platform, + fileSystem: fileSystem, + userMessages: UserMessages(), + ); - await bundle.build( - packagesPath: '.packages', - flutterProject: FlutterProject.fromDirectoryTest(testFileSystem.currentDirectory), - flavor: flavor, - ); - return bundle; - } - - testWithoutContext('correctly bundles assets given a simple asset manifest with flavors', () async { - testFileSystem.file('.packages').createSync(); - testFileSystem.file(testFileSystem.path.join('assets', 'common', 'image.png')).createSync(recursive: true); - testFileSystem.file(testFileSystem.path.join('assets', 'vanilla', 'ice-cream.png')).createSync(recursive: true); - testFileSystem.file(testFileSystem.path.join('assets', 'strawberry', 'ice-cream.png')).createSync(recursive: true); - testFileSystem.file(testFileSystem.path.join('assets', 'orange', 'ice-cream.png')).createSync(recursive: true); - testFileSystem.file('pubspec.yaml') - ..createSync() - ..writeAsStringSync(r''' + fileSystem.file('.packages').createSync(); + fileSystem.file('pubspec.yaml') + ..createSync() + ..writeAsStringSync(r''' name: example flutter: assets: - - assets/common/ - - path: assets/vanilla/ - flavors: - - vanilla - - path: assets/strawberry/ - flavors: - - strawberry - - path: assets/orange/ice-cream.png - flavors: - - orange - '''); + - https://mywebsite.com/images/ +'''); + final ManifestAssetBundle bundle = ManifestAssetBundle( + logger: logger, + fileSystem: fileSystem, + platform: platform, + flutterRoot: flutterRoot, + ); - ManifestAssetBundle bundle; - bundle = await buildBundleWithFlavor(null); - expect(bundle.entries.keys, contains('assets/common/image.png')); - expect(bundle.entries.keys, isNot(contains('assets/vanilla/ice-cream.png'))); - expect(bundle.entries.keys, isNot(contains('assets/strawberry/ice-cream.png'))); - expect(bundle.entries.keys, isNot(contains('assets/orange/ice-cream.png'))); - - bundle = await buildBundleWithFlavor('strawberry'); - expect(bundle.entries.keys, contains('assets/common/image.png')); - expect(bundle.entries.keys, isNot(contains('assets/vanilla/ice-cream.png'))); - expect(bundle.entries.keys, contains('assets/strawberry/ice-cream.png')); - expect(bundle.entries.keys, isNot(contains('assets/orange/ice-cream.png'))); - - bundle = await buildBundleWithFlavor('orange'); - expect(bundle.entries.keys, contains('assets/common/image.png')); - expect(bundle.entries.keys, isNot(contains('assets/vanilla/ice-cream.png'))); - expect(bundle.entries.keys, isNot(contains('assets/strawberry/ice-cream.png'))); - expect(bundle.entries.keys, contains('assets/orange/ice-cream.png')); - }); - - testWithoutContext('throws a tool exit when a non-flavored folder contains a flavored asset', () async { - testFileSystem.file('.packages').createSync(); - testFileSystem.file(testFileSystem.path.join('assets', 'unflavored.png')).createSync(recursive: true); - testFileSystem.file(testFileSystem.path.join('assets', 'vanillaOrange.png')).createSync(recursive: true); - - testFileSystem.file('pubspec.yaml') - ..createSync() - ..writeAsStringSync(r''' - name: example - flutter: - assets: - - assets/ - - path: assets/vanillaOrange.png - flavors: - - vanilla - - orange - '''); - - expect( - buildBundleWithFlavor(null), - throwsToolExit(message: 'Multiple assets entries include the file ' - '"assets/vanillaOrange.png", but they specify different lists of flavors.\n' - 'An entry with the path "assets/" does not specify any flavors.\n' - 'An entry with the path "assets/vanillaOrange.png" specifies the flavor(s): "vanilla", "orange".\n\n' - 'Consider organizing assets with different flavors into different directories.'), - ); - }); - - testWithoutContext('throws a tool exit when a flavored folder contains a flavorless asset', () async { - testFileSystem.file('.packages').createSync(); - testFileSystem.file(testFileSystem.path.join('vanilla', 'vanilla.png')).createSync(recursive: true); - testFileSystem.file(testFileSystem.path.join('vanilla', 'flavorless.png')).createSync(recursive: true); - - testFileSystem.file('pubspec.yaml') - ..createSync() - ..writeAsStringSync(r''' - name: example - flutter: - assets: - - path: vanilla/ - flavors: - - vanilla - - vanilla/flavorless.png - '''); - expect( - buildBundleWithFlavor(null), - throwsToolExit(message: 'Multiple assets entries include the file ' - '"vanilla/flavorless.png", but they specify different lists of flavors.\n' - 'An entry with the path "vanilla/" specifies the flavor(s): "vanilla".\n' - 'An entry with the path "vanilla/flavorless.png" does not specify any flavors.\n\n' - 'Consider organizing assets with different flavors into different directories.'), - ); - }); - - testWithoutContext('tool exits when two file-explicit entries give the same asset different flavors', () { - testFileSystem.file('.packages').createSync(); - testFileSystem.file('orange.png').createSync(recursive: true); - testFileSystem.file('pubspec.yaml') - ..createSync() - ..writeAsStringSync(r''' - name: example - flutter: - assets: - - path: orange.png - flavors: - - orange - - path: orange.png - flavors: - - mango - '''); - - expect( - buildBundleWithFlavor(null), - throwsToolExit(message: 'Multiple assets entries include the file ' - '"orange.png", but they specify different lists of flavors.\n' - 'An entry with the path "orange.png" specifies the flavor(s): "orange".\n' - 'An entry with the path "orange.png" specifies the flavor(s): "mango".'), - ); + expect( + () => bundle.build( + packagesPath: '.packages', + flutterProject: FlutterProject.fromDirectoryTest( + fileSystem.currentDirectory, + ), + ), + throwsToolExit( + message: 'Unable to search for asset files in directory path "https%3A//mywebsite.com/images/". ' + 'Please ensure that this entry in pubspec.yaml is a valid file path.\n' + 'Error details:\n' + 'Unsupported operation: Illegal character in path: https:', + ), + ); }); - testWithoutContext('throws ToolExit when flavor from file-level declaration has different flavor from containing folder flavor declaration', () async { - testFileSystem.file('.packages').createSync(); - testFileSystem.file(testFileSystem.path.join('vanilla', 'actually-strawberry.png')).createSync(recursive: true); - testFileSystem.file(testFileSystem.path.join('vanilla', 'vanilla.png')).createSync(recursive: true); + testWithoutContext('throws ToolExit when file entry contains invalid characters (Windows only)', () async { + final FileSystem fileSystem = MemoryFileSystem( + style: FileSystemStyle.windows, + opHandle: (String context, FileSystemOp operation) { + if (operation == FileSystemOp.exists && context == r'C:\http:\\website.com') { + throw const FileSystemException( + r"FileSystemException: Exists failed, path = 'C:\http:\\website.com' " + '(OS Error: The filename, directory name, or volume label syntax is ' + 'incorrect., errno = 123)', + ); + } + }, + ); + final BufferLogger logger = BufferLogger.test(); + final FakePlatform platform = FakePlatform(operatingSystem: 'windows'); + final String flutterRoot = Cache.defaultFlutterRoot( + platform: platform, + fileSystem: fileSystem, + userMessages: UserMessages(), + ); + fileSystem.file('.packages').createSync(); + fileSystem.file('pubspec.yaml') + ..createSync() + ..writeAsStringSync(r''' +name: example +flutter: + assets: + - http://website.com/hi.png +'''); + final ManifestAssetBundle bundle = ManifestAssetBundle( + logger: logger, + fileSystem: fileSystem, + platform: platform, + flutterRoot: flutterRoot, + ); - testFileSystem.file('pubspec.yaml') - ..createSync() - ..writeAsStringSync(r''' - name: example - flutter: - assets: - - path: vanilla/ - flavors: - - vanilla - - path: vanilla/actually-strawberry.png - flavors: - - strawberry - '''); - expect( - buildBundleWithFlavor(null), - throwsToolExit(message: 'Multiple assets entries include the file ' - '"vanilla/actually-strawberry.png", but they specify different lists of flavors.\n' - 'An entry with the path "vanilla/" specifies the flavor(s): "vanilla".\n' - 'An entry with the path "vanilla/actually-strawberry.png" ' - 'specifies the flavor(s): "strawberry".'), - ); - }); + expect( + () => bundle.build( + packagesPath: '.packages', + flutterProject: FlutterProject.fromDirectoryTest( + fileSystem.currentDirectory, + ), + ), + throwsToolExit( + message: 'Unable to check the existence of asset file ', + ), + ); }); });