Unverified Commit 368da5bb authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

[flutter_tools] support bundling SkSL shaders in flutter build apk/appbundle (#56059)

Support bundling SkSL shaders into an android APK or appbundle via the --bundle-sksl-path command line options. If provided, these are validated for platform engine revision and then placed in flutter_assets/io.flutter.shaders.json
parent 0d452b83
......@@ -619,6 +619,10 @@ class FlutterPlugin implements Plugin<Project> {
if (project.hasProperty('dart-defines')) {
dartDefinesValue = project.property('dart-defines')
}
String bundleSkSLPathValue;
if (project.hasProperty('bundle-sksl-path')) {
bundleSkSLPathValue = project.property('bundle-sksl-path')
}
def targetPlatforms = getTargetPlatforms()
def addFlutterDeps = { variant ->
if (shouldSplitPerAbi()) {
......@@ -657,6 +661,7 @@ class FlutterPlugin implements Plugin<Project> {
treeShakeIcons treeShakeIconsOptionsValue
dartObfuscation dartObfuscationValue
dartDefines dartDefinesValue
bundleSkSLPath bundleSkSLPathValue
doLast {
project.exec {
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
......@@ -849,6 +854,8 @@ abstract class BaseFlutterTask extends DefaultTask {
Boolean dartObfuscation
@Optional @Input
String dartDefines
@Optional @Input
String bundleSkSLPath
@OutputFiles
FileCollection getDependenciesFiles() {
......@@ -918,6 +925,9 @@ abstract class BaseFlutterTask extends DefaultTask {
if (dartDefines != null) {
args "--DartDefines=${dartDefines}"
}
if (bundleSkSLPath != null) {
args "-iBundleSkSLPath=${bundleSkSLPath}"
}
if (extraGenSnapshotOptions != null) {
args "--ExtraGenSnapshotOptions=${extraGenSnapshotOptions}"
}
......
......@@ -351,6 +351,9 @@ Future<void> buildGradleApp({
if (androidBuildInfo.buildInfo.dartObfuscation) {
command.add('-Pdart-obfuscation=true');
}
if (androidBuildInfo.buildInfo.bundleSkSLPath != null) {
command.add('-Pbundle-sksl-path=${androidBuildInfo.buildInfo.bundleSkSLPath}');
}
command.add(assembleTask);
GradleHandledError detectedGradleError;
......
......@@ -61,13 +61,14 @@ class _ManifestAssetBundleFactory implements AssetBundleFactory {
const _ManifestAssetBundleFactory();
@override
AssetBundle createBundle() => _ManifestAssetBundle();
AssetBundle createBundle() => ManifestAssetBundle();
}
class _ManifestAssetBundle implements AssetBundle {
/// Constructs an [_ManifestAssetBundle] that gathers the set of assets from the
/// An asset bundle based on a pubspec.yaml
class ManifestAssetBundle implements AssetBundle {
/// Constructs an [ManifestAssetBundle] that gathers the set of assets from the
/// pubspec.yaml manifest.
_ManifestAssetBundle();
ManifestAssetBundle();
@override
final Map<String, DevFSContent> entries = <String, DevFSContent>{};
......@@ -493,7 +494,7 @@ List<Map<String, dynamic>> _parseFonts(
}) {
return <Map<String, dynamic>>[
if (manifest.usesMaterialDesign && includeDefaultFonts)
..._getMaterialFonts(_ManifestAssetBundle._fontSetMaterial),
..._getMaterialFonts(ManifestAssetBundle._fontSetMaterial),
if (packageName == null)
...manifest.fontsDescriptor
else
......
......@@ -24,6 +24,7 @@ class BuildInfo {
this.splitDebugInfoPath,
this.dartObfuscation = false,
this.dartDefines = const <String>[],
this.bundleSkSLPath,
this.dartExperiments = const <String>[],
@required this.treeShakeIcons,
});
......@@ -74,6 +75,11 @@ class BuildInfo {
/// Whether to apply dart source code obfuscation.
final bool dartObfuscation;
/// An optional path to a JSON containing object SkSL shaders
///
/// Currently this is only supported for Android builds.
final String bundleSkSLPath;
/// Additional constant values to be made available in the Dart program.
///
/// These values can be used with the const `fromEnvironment` constructors of
......
......@@ -6,6 +6,7 @@ import '../../artifacts.dart';
import '../../base/build.dart';
import '../../base/file_system.dart';
import '../../build_info.dart';
import '../../devfs.dart';
import '../../globals.dart' as globals;
import '../build_system.dart';
import '../depfile.dart';
......@@ -62,7 +63,27 @@ abstract class AndroidAssetBundle extends Target {
.copySync(outputDirectory.childFile('isolate_snapshot_data').path);
}
if (_copyAssets) {
final Depfile assetDepfile = await copyAssets(environment, outputDirectory);
final String shaderBundlePath = environment.inputs[kBundleSkSLPath];
final DevFSContent skslBundle = processSkSLBundle(
shaderBundlePath,
engineVersion: environment.engineVersion,
fileSystem: environment.fileSystem,
logger: environment.logger,
targetPlatform: TargetPlatform.android,
);
final Depfile assetDepfile = await copyAssets(
environment,
outputDirectory,
additionalContent: <String, DevFSContent>{
if (skslBundle != null)
kSkSLShaderBundlePath: skslBundle,
}
);
if (shaderBundlePath != null) {
final File skSLBundleFile = environment.fileSystem
.file(shaderBundlePath).absolute;
assetDepfile.inputs.add(skSLBundleFile);
}
final DepfileService depfileService = DepfileService(
fileSystem: globals.fs,
logger: globals.logger,
......
......@@ -2,10 +2,14 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:meta/meta.dart';
import 'package:pool/pool.dart';
import '../../asset.dart';
import '../../base/file_system.dart';
import '../../base/logger.dart';
import '../../build_info.dart';
import '../../convert.dart';
import '../../devfs.dart';
import '../../globals.dart' as globals;
import '../build_system.dart';
......@@ -13,13 +17,21 @@ import '../depfile.dart';
import 'dart.dart';
import 'icon_tree_shaker.dart';
/// The input key for an SkSL bundle path.
const String kBundleSkSLPath = 'BundleSkSLPath';
/// A helper function to copy an asset bundle into an [environment]'s output
/// directory.
///
/// Throws [Exception] if [AssetBundle.build] returns a non-zero exit code.
///
/// [additionalContent] may contain additional DevFS entries that will be
/// included in the final bundle, but not the AssetManifest.json file.
///
/// Returns a [Depfile] containing all assets used in the build.
Future<Depfile> copyAssets(Environment environment, Directory outputDirectory) async {
Future<Depfile> copyAssets(Environment environment, Directory outputDirectory, {
Map<String, DevFSContent> additionalContent,
}) async {
final File pubspecFile = environment.projectDir.childFile('pubspec.yaml');
final AssetBundle assetBundle = AssetBundleFactory.instance.createBundle();
final int resultCode = await assetBundle.build(
......@@ -46,8 +58,13 @@ Future<Depfile> copyAssets(Environment environment, Directory outputDirectory) a
artifacts: globals.artifacts,
);
final Map<String, DevFSContent> assetEntries = <String, DevFSContent>{
...assetBundle.entries,
...?additionalContent,
};
await Future.wait<void>(
assetBundle.entries.entries.map<Future<void>>((MapEntry<String, DevFSContent> entry) async {
assetEntries.entries.map<Future<void>>((MapEntry<String, DevFSContent> entry) async {
final PoolResource resource = await pool.request();
try {
// This will result in strange looking files, for example files with `/`
......@@ -78,6 +95,76 @@ Future<Depfile> copyAssets(Environment environment, Directory outputDirectory) a
return Depfile(inputs + assetBundle.additionalDependencies, outputs);
}
/// The path of the SkSL JSON bundle included in flutter_assets.
const String kSkSLShaderBundlePath = 'io.flutter.shaders.json';
/// Validate and process an SkSL asset bundle in a [DevFSContent].
///
/// Returns `null` if the bundle was not provided, otherwise attempts to
/// validate the bundle.
///
/// Throws [Exception] if the bundle is invalid due to formatting issues.
///
/// If the current target platform is different than the platform constructed
/// for the bundle, a warning will be printed.
DevFSContent processSkSLBundle(String bundlePath, {
@required TargetPlatform targetPlatform,
@required FileSystem fileSystem,
@required Logger logger,
@required String engineVersion,
}) {
if (bundlePath == null) {
return null;
}
// Step 1: check that file exists.
final File skSLBundleFile = fileSystem.file(bundlePath);
if (!skSLBundleFile.existsSync()) {
logger.printError('$bundlePath does not exist.');
throw Exception('SkSL bundle was invalid.');
}
// Step 2: validate top level bundle structure.
Map<String, Object> bundle;
try {
final Object rawBundle = json.decode(skSLBundleFile.readAsStringSync());
if (rawBundle is Map<String, Object>) {
bundle = rawBundle;
} else {
logger.printError('"$bundle" was not a JSON object: $rawBundle');
throw Exception('SkSL bundle was invalid.');
}
} on FormatException catch (err) {
logger.printError('"$bundle" was not a JSON object: $err');
throw Exception('SkSL bundle was invalid.');
}
// Step 3: Validate that:
// * The engine revision the bundle was compiled with
// is the same as the current revision.
// * The target platform is the same (this one is a warning only).
final String bundleEngineRevision = bundle['engineRevision'] as String;
if (bundleEngineRevision != engineVersion) {
logger.printError(
'Expected Flutter $bundleEngineRevision, but found $engineVersion\n'
'The SkSL bundle was produced with a different engine version. It must '
'be recreated for the current Flutter version.'
);
throw Exception('SkSL bundle was invalid');
}
final TargetPlatform bundleTargetPlatform = getTargetPlatformForName(
bundle['platform'] as String);
if (bundleTargetPlatform != targetPlatform) {
logger.printError(
'The SkSL bundle was created for $bundleTargetPlatform, but the curent '
'platform is $targetPlatform. This may lead to less efficient shader '
'caching.'
);
}
return DevFSStringContent(json.encode(<String, Object>{
'data': bundle['data'],
}));
}
/// Copy the assets defined in the flutter manifest into a build directory.
class CopyAssets extends Target {
const CopyAssets();
......
......@@ -19,7 +19,7 @@ import 'assets.dart';
import 'icon_tree_shaker.dart';
/// The define to pass a [BuildMode].
const String kBuildMode= 'BuildMode';
const String kBuildMode = 'BuildMode';
/// The define to pass whether we compile 64-bit android-arm code.
const String kTargetPlatform = 'TargetPlatform';
......
......@@ -293,13 +293,14 @@ abstract class MacOSBundleFlutterAssets extends Target {
.childDirectory('Resources')
.childDirectory('flutter_assets');
assetDirectory.createSync(recursive: true);
final Depfile depfile = await copyAssets(environment, assetDirectory);
final Depfile assetDepfile = await copyAssets(environment, assetDirectory);
final DepfileService depfileService = DepfileService(
fileSystem: globals.fs,
logger: globals.logger,
);
depfileService.writeToFile(
depfile,
assetDepfile,
environment.buildDir.childFile('flutter_assets.d'),
);
......
......@@ -30,6 +30,7 @@ class BuildApkCommand extends BuildSubCommand {
addDartObfuscationOption();
usesDartDefineOption();
usesExtraFrontendOptions();
addBundleSkSLPathOption(hide: !verboseHelp);
addEnableExperimentation(hide: !verboseHelp);
argParser
..addFlag('split-per-abi',
......
......@@ -28,6 +28,7 @@ class BuildAppBundleCommand extends BuildSubCommand {
addDartObfuscationOption();
usesDartDefineOption();
usesExtraFrontendOptions();
addBundleSkSLPathOption(hide: !verboseHelp);
argParser
..addFlag('track-widget-creation', negatable: false, hide: !verboseHelp)
..addMultiOption('target-platform',
......
......@@ -834,8 +834,21 @@ abstract class ResidentRunner {
'sksl',
);
final Device device = flutterDevices.first.device;
// Convert android sub-platforms to single target platform.
TargetPlatform targetPlatform = await flutterDevices.first.device.targetPlatform;
switch (targetPlatform) {
case TargetPlatform.android_arm:
case TargetPlatform.android_arm64:
case TargetPlatform.android_x64:
case TargetPlatform.android_x86:
targetPlatform = TargetPlatform.android;
break;
default:
break;
}
final Map<String, Object> manifest = <String, Object>{
'platform': getNameForTargetPlatform(await flutterDevices.first.device.targetPlatform),
'platform': getNameForTargetPlatform(targetPlatform),
'name': device.name,
'engineRevision': globals.flutterVersion.engineRevision,
'data': data,
......
......@@ -108,6 +108,7 @@ class FlutterOptions {
static const String kSplitDebugInfoOption = 'split-debug-info';
static const String kDartObfuscationOption = 'obfuscate';
static const String kDartDefinesOption = 'dart-define';
static const String kBundleSkSLPathOption = 'bundle-sksl-path';
}
abstract class FlutterCommand extends Command<void> {
......@@ -425,6 +426,16 @@ abstract class FlutterCommand extends Command<void> {
);
}
void addBundleSkSLPathOption({ @required bool hide }) {
argParser.addOption(FlutterOptions.kBundleSkSLPathOption,
help: 'A path to a file containing precompiled SkSL shaders generated '
'during "flutter run". These can be included in an application to '
'improve the first frame render times.',
hide: hide,
valueHelp: '/project-name/flutter_1.sksl'
);
}
void addTreeShakeIconsFlag({
bool enabledByDefault
}) {
......@@ -587,6 +598,10 @@ abstract class FlutterCommand extends Command<void> {
&& buildMode.isPrecompiled
&& boolArg('tree-shake-icons');
final String bundleSkSLPath = argParser.options.containsKey(FlutterOptions.kBundleSkSLPathOption)
? stringArg(FlutterOptions.kBundleSkSLPathOption)
: null;
return BuildInfo(buildMode,
argParser.options.containsKey('flavor')
? stringArg('flavor')
......@@ -614,6 +629,7 @@ abstract class FlutterCommand extends Command<void> {
dartDefines: argParser.options.containsKey(FlutterOptions.kDartDefinesOption)
? stringsArg(FlutterOptions.kDartDefinesOption)
: const <String>[],
bundleSkSLPath: bundleSkSLPath,
dartExperiments: experiments,
);
}
......
......@@ -2,12 +2,15 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:file_testing/file_testing.dart';
import 'package:flutter_tools/src/artifacts.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/build_system/build_system.dart';
import 'package:flutter_tools/src/build_system/targets/android.dart';
import 'package:flutter_tools/src/build_system/targets/assets.dart';
import 'package:flutter_tools/src/build_system/targets/dart.dart';
import 'package:flutter_tools/src/convert.dart';
import 'package:flutter_tools/src/globals.dart' as globals;
import 'package:flutter_tools/src/cache.dart';
import 'package:mockito/mockito.dart';
......@@ -58,6 +61,50 @@ void main() {
expect(globals.fs.file(globals.fs.path.join('out', 'flutter_assets', 'kernel_blob.bin')).existsSync(), true);
});
testbed.test('debug bundle contains expected resources with bundle SkSL', () async {
final Environment environment = Environment.test(
globals.fs.currentDirectory,
outputDir: globals.fs.directory('out')..createSync(),
defines: <String, String>{
kBuildMode: 'debug',
},
inputs: <String, String>{
kBundleSkSLPath: 'bundle.sksl'
},
processManager: fakeProcessManager,
artifacts: MockArtifacts(),
fileSystem: globals.fs,
logger: globals.logger,
engineVersion: '2',
);
environment.buildDir.createSync(recursive: true);
globals.fs.file('bundle.sksl').writeAsStringSync(json.encode(
<String, Object>{
'engineRevision': '2',
'platform': 'android',
'data': <String, Object>{
'A': 'B',
}
}
));
// create pre-requisites.
environment.buildDir.childFile('app.dill')
.writeAsStringSync('abcd');
final Directory hostDirectory = globals.fs.currentDirectory
.childDirectory(getNameForHostPlatform(getCurrentHostPlatform()))
..createSync(recursive: true);
hostDirectory.childFile('vm_isolate_snapshot.bin').createSync();
hostDirectory.childFile('isolate_snapshot.bin').createSync();
await const DebugAndroidApplication().build(environment);
expect(globals.fs.file(globals.fs.path.join('out', 'flutter_assets', 'isolate_snapshot_data')), exists);
expect(globals.fs.file(globals.fs.path.join('out', 'flutter_assets', 'vm_snapshot_data')), exists);
expect(globals.fs.file(globals.fs.path.join('out', 'flutter_assets', 'kernel_blob.bin')), exists);
expect(globals.fs.file(globals.fs.path.join('out', 'flutter_assets', 'io.flutter.shaders.json')), exists);
});
testbed.test('profile bundle contains expected resources', () async {
final Environment environment = Environment.test(
globals.fs.currentDirectory,
......
......@@ -7,9 +7,12 @@ import 'package:file_testing/file_testing.dart';
import 'package:flutter_tools/src/artifacts.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/build_system/build_system.dart';
import 'package:flutter_tools/src/build_system/depfile.dart';
import 'package:flutter_tools/src/build_system/targets/assets.dart';
import 'package:flutter_tools/src/convert.dart';
import 'package:flutter_tools/src/devfs.dart';
import 'package:mockito/mockito.dart';
import 'package:platform/platform.dart';
......@@ -116,6 +119,134 @@ flutter:
ProcessManager: () => FakeProcessManager.any(),
Platform: () => platform,
});
testWithoutContext('processSkSLBundle returns null if there is no path '
'to the bundle', () {
expect(processSkSLBundle(
null,
targetPlatform: TargetPlatform.android,
fileSystem: MemoryFileSystem.test(),
logger: BufferLogger.test(),
engineVersion: null,
), isNull);
});
testWithoutContext('processSkSLBundle throws exception if bundle file is '
'missing', () {
expect(() => processSkSLBundle(
'does_not_exist.sksl',
targetPlatform: TargetPlatform.android,
fileSystem: MemoryFileSystem.test(),
logger: BufferLogger.test(),
engineVersion: null,
), throwsA(isA<Exception>()));
});
testWithoutContext('processSkSLBundle throws exception if the bundle is not '
'valid JSON', () {
final FileSystem fileSystem = MemoryFileSystem.test();
final BufferLogger logger = BufferLogger.test();
fileSystem.file('bundle.sksl').writeAsStringSync('{');
expect(() => processSkSLBundle(
'bundle.sksl',
targetPlatform: TargetPlatform.android,
fileSystem: fileSystem,
logger: logger,
engineVersion: null,
), throwsA(isA<Exception>()));
expect(logger.errorText, contains('was not a JSON object'));
});
testWithoutContext('processSkSLBundle throws exception if the bundle is not '
'a JSON object', () {
final FileSystem fileSystem = MemoryFileSystem.test();
final BufferLogger logger = BufferLogger.test();
fileSystem.file('bundle.sksl').writeAsStringSync('[]');
expect(() => processSkSLBundle(
'bundle.sksl',
targetPlatform: TargetPlatform.android,
fileSystem: fileSystem,
logger: logger,
engineVersion: null,
), throwsA(isA<Exception>()));
expect(logger.errorText, contains('was not a JSON object'));
});
testWithoutContext('processSkSLBundle throws an exception if the engine '
'revision is different', () {
final FileSystem fileSystem = MemoryFileSystem.test();
final BufferLogger logger = BufferLogger.test();
fileSystem.file('bundle.sksl').writeAsStringSync(json.encode(
<String, String>{
'engineRevision': '1'
}
));
expect(() => processSkSLBundle(
'bundle.sksl',
targetPlatform: TargetPlatform.android,
fileSystem: fileSystem,
logger: logger,
engineVersion: '2',
), throwsA(isA<Exception>()));
expect(logger.errorText, contains('Expected Flutter 1, but found 2'));
});
testWithoutContext('processSkSLBundle warns if the bundle target platform is '
'different from the current target', () async {
final FileSystem fileSystem = MemoryFileSystem.test();
final BufferLogger logger = BufferLogger.test();
fileSystem.file('bundle.sksl').writeAsStringSync(json.encode(
<String, Object>{
'engineRevision': '2',
'platform': 'fuchsia',
'data': <String, Object>{}
}
));
final DevFSContent content = processSkSLBundle(
'bundle.sksl',
targetPlatform: TargetPlatform.android,
fileSystem: fileSystem,
logger: logger,
engineVersion: '2',
);
expect(await content.contentsAsBytes(), utf8.encode('{"data":{}}'));
expect(logger.errorText, contains('This may lead to less efficient shader caching'));
});
testWithoutContext('processSkSLBundle does not warn and produces bundle', () async {
final FileSystem fileSystem = MemoryFileSystem.test();
final BufferLogger logger = BufferLogger.test();
fileSystem.file('bundle.sksl').writeAsStringSync(json.encode(
<String, Object>{
'engineRevision': '2',
'platform': 'android',
'data': <String, Object>{}
}
));
final DevFSContent content = processSkSLBundle(
'bundle.sksl',
targetPlatform: TargetPlatform.android,
fileSystem: fileSystem,
logger: logger,
engineVersion: '2',
);
expect(await content.contentsAsBytes(), utf8.encode('{"data":{}}'));
expect(logger.errorText, isEmpty);
});
}
class MockArtifacts extends Mock implements Artifacts {}
......@@ -627,7 +627,7 @@ void main() {
expect(testLogger.statusText, contains('flutter_01.sksl'));
expect(globals.fs.file('flutter_01.sksl'), exists);
expect(json.decode(globals.fs.file('flutter_01.sksl').readAsStringSync()), <String, Object>{
'platform': 'android-arm',
'platform': 'android',
'name': 'test device',
'engineRevision': '42.2', // From FakeFlutterVersion
'data': <String, Object>{'A': 'B'}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment