assets.dart 12.3 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5 6 7 8
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:pool/pool.dart';

import '../../asset.dart';
import '../../base/file_system.dart';
9 10 11
import '../../base/logger.dart';
import '../../build_info.dart';
import '../../convert.dart';
12 13
import '../../devfs.dart';
import '../build_system.dart';
14
import '../depfile.dart';
15
import 'common.dart';
16
import 'icon_tree_shaker.dart';
17
import 'scene_importer.dart';
18
import 'shader_compiler.dart';
19 20 21 22

/// A helper function to copy an asset bundle into an [environment]'s output
/// directory.
///
23 24
/// Throws [Exception] if [AssetBundle.build] returns a non-zero exit code.
///
25 26 27
/// [additionalContent] may contain additional DevFS entries that will be
/// included in the final bundle, but not the AssetManifest.json file.
///
28
/// Returns a [Depfile] containing all assets used in the build.
29 30 31
Future<Depfile> copyAssets(
  Environment environment,
  Directory outputDirectory, {
32 33 34
  Map<String, DevFSContent>? additionalContent,
  required TargetPlatform targetPlatform,
  BuildMode? buildMode,
35
  required ShaderTarget shaderTarget,
36
  List<File> additionalInputs = const <File>[],
37
}) async {
38
  // Check for an SkSL bundle.
39 40
  final String? shaderBundlePath = environment.defines[kBundleSkSLPath] ?? environment.inputs[kBundleSkSLPath];
  final DevFSContent? skslBundle = processSkSLBundle(
41 42 43 44 45 46 47
    shaderBundlePath,
    engineVersion: environment.engineVersion,
    fileSystem: environment.fileSystem,
    logger: environment.logger,
    targetPlatform: targetPlatform,
  );

48
  final File pubspecFile =  environment.projectDir.childFile('pubspec.yaml');
49
  // Only the default asset bundle style is supported in assemble.
50 51 52
  final AssetBundle assetBundle = AssetBundleFactory.defaultInstance(
    logger: environment.logger,
    fileSystem: environment.fileSystem,
53
    platform: environment.platform,
54
    splitDeferredAssets: buildMode != BuildMode.debug && buildMode != BuildMode.jitRelease,
55
  ).createBundle();
56
  final int resultCode = await assetBundle.build(
57 58
    manifestPath: pubspecFile.path,
    packagesPath: environment.projectDir.childFile('.packages').path,
59
    deferredComponentsEnabled: environment.defines[kDeferredComponents] == 'true',
60
    targetPlatform: targetPlatform,
61
  );
62 63 64
  if (resultCode != 0) {
    throw Exception('Failed to bundle asset files.');
  }
65 66 67 68 69
  final Pool pool = Pool(kMaxOpenFiles);
  final List<File> inputs = <File>[
    // An asset manifest with no assets would have zero inputs if not
    // for this pubspec file.
    pubspecFile,
70
    ...additionalInputs,
71 72
  ];
  final List<File> outputs = <File>[];
73 74 75

  final IconTreeShaker iconTreeShaker = IconTreeShaker(
    environment,
76
    assetBundle.entries[kFontManifestJson] as DevFSStringContent?,
77 78 79 80
    processManager: environment.processManager,
    logger: environment.logger,
    fileSystem: environment.fileSystem,
    artifacts: environment.artifacts,
81
    targetPlatform: targetPlatform,
82
  );
83 84 85 86 87 88
  final ShaderCompiler shaderCompiler = ShaderCompiler(
    processManager: environment.processManager,
    logger: environment.logger,
    fileSystem: environment.fileSystem,
    artifacts: environment.artifacts,
  );
89 90 91 92 93 94
  final SceneImporter sceneImporter = SceneImporter(
    processManager: environment.processManager,
    logger: environment.logger,
    fileSystem: environment.fileSystem,
    artifacts: environment.artifacts,
  );
95

96 97 98
  final Map<String, DevFSContent> assetEntries = <String, DevFSContent>{
    ...assetBundle.entries,
    ...?additionalContent,
99 100
    if (skslBundle != null)
      kSkSLShaderBundlePath: skslBundle,
101
  };
102 103 104
  final Map<String, AssetKind> entryKinds = <String, AssetKind>{
    ...assetBundle.entryKinds,
  };
105

106
  await Future.wait<void>(
107
    assetEntries.entries.map<Future<void>>((MapEntry<String, DevFSContent> entry) async {
108 109
      final PoolResource resource = await pool.request();
      try {
Dan Field's avatar
Dan Field committed
110 111
        // This will result in strange looking files, for example files with `/`
        // on Windows or files that end up getting URI encoded such as `#.ext`
112
        // to `%23.ext`. However, we have to keep it this way since the
Dan Field's avatar
Dan Field committed
113 114
        // platform channels in the framework will URI encode these values,
        // and the native APIs will look for files this way.
115 116
        final File file = environment.fileSystem.file(
          environment.fileSystem.path.join(outputDirectory.path, entry.key));
117
        final AssetKind assetKind = entryKinds[entry.key] ?? AssetKind.regular;
118 119 120 121
        outputs.add(file);
        file.parent.createSync(recursive: true);
        final DevFSContent content = entry.value;
        if (content is DevFSFileContent && content.file is File) {
122
          inputs.add(content.file as File);
123 124 125 126 127 128 129 130 131 132 133 134 135 136
          bool doCopy = true;
          switch (assetKind) {
            case AssetKind.regular:
              break;
            case AssetKind.font:
              doCopy = !await iconTreeShaker.subsetFont(
                input: content.file as File,
                outputPath: file.path,
                relativePath: entry.key,
              );
            case AssetKind.shader:
              doCopy = !await shaderCompiler.compileShader(
                input: content.file as File,
                outputPath: file.path,
137
                target: shaderTarget,
138
                json: targetPlatform == TargetPlatform.web_javascript,
139
              );
140 141 142 143 144
            case AssetKind.model:
              doCopy = !await sceneImporter.importScene(
                input: content.file as File,
                outputPath: file.path,
              );
145 146
          }
          if (doCopy) {
147 148
            await (content.file as File).copy(file.path);
          }
149 150 151 152 153 154 155
        } else {
          await file.writeAsBytes(await entry.value.contentsAsBytes());
        }
      } finally {
        resource.release();
      }
  }));
156 157 158 159

  // Copy deferred components assets only for release or profile builds.
  // The assets are included in assetBundle.entries as a normal asset when
  // building as debug.
160
  if (environment.defines[kDeferredComponents] == 'true' && buildMode != null) {
161 162
    await Future.wait<void>(assetBundle.deferredComponentsEntries.entries.map<Future<void>>(
      (MapEntry<String, Map<String, DevFSContent>> componentEntries) async {
163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181
        final Directory componentOutputDir =
            environment.projectDir
                .childDirectory('build')
                .childDirectory(componentEntries.key)
                .childDirectory('intermediates')
                .childDirectory('flutter');
        await Future.wait<void>(
          componentEntries.value.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 `/`
              // on Windows or files that end up getting URI encoded such as `#.ext`
              // to `%23.ext`. However, we have to keep it this way since the
              // platform channels in the framework will URI encode these values,
              // and the native APIs will look for files this way.

              // If deferred components are disabled, then copy assets to regular location.
              final File file = environment.defines[kDeferredComponents] == 'true'
                ? environment.fileSystem.file(
182
                    environment.fileSystem.path.join(componentOutputDir.path, buildMode.cliName, 'deferred_assets', 'flutter_assets', entry.key))
183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205
                : environment.fileSystem.file(
                    environment.fileSystem.path.join(outputDirectory.path, entry.key));
              outputs.add(file);
              file.parent.createSync(recursive: true);
              final DevFSContent content = entry.value;
              if (content is DevFSFileContent && content.file is File) {
                inputs.add(content.file as File);
                if (!await iconTreeShaker.subsetFont(
                  input: content.file as File,
                  outputPath: file.path,
                  relativePath: entry.key,
                )) {
                  await (content.file as File).copy(file.path);
                }
              } else {
                await file.writeAsBytes(await entry.value.contentsAsBytes());
              }
            } finally {
              resource.release();
            }
        }));
    }));
  }
206 207 208 209 210 211 212
  final Depfile depfile = Depfile(inputs + assetBundle.additionalDependencies, outputs);
  if (shaderBundlePath != null) {
    final File skSLBundleFile = environment.fileSystem
      .file(shaderBundlePath).absolute;
    depfile.inputs.add(skSLBundleFile);
  }
  return depfile;
213 214
}

215 216 217 218 219 220 221 222 223 224 225 226
/// 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.
227 228 229 230 231
DevFSContent? processSkSLBundle(String? bundlePath, {
  required TargetPlatform targetPlatform,
  required FileSystem fileSystem,
  required Logger logger,
  String? engineVersion,
232 233 234 235 236 237 238 239 240 241 242 243
}) {
  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.
244
  Map<String, Object?>? bundle;
245
  try {
246
    final Object? rawBundle = json.decode(skSLBundleFile.readAsStringSync());
247
    if (rawBundle is Map<String, Object?>) {
248 249 250 251 252 253 254 255 256 257 258 259 260
      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).
261
  final String? bundleEngineRevision = bundle['engineRevision'] as String?;
262 263 264 265 266 267 268 269 270
  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');
  }

271 272 273 274 275 276
  final String? parsedPlatform = bundle['platform'] as String?;
  TargetPlatform? bundleTargetPlatform;
  if (parsedPlatform != null) {
    bundleTargetPlatform = getTargetPlatformForName(parsedPlatform);
  }
  if (bundleTargetPlatform == null || bundleTargetPlatform != targetPlatform) {
277
    logger.printError(
278
      'The SkSL bundle was created for $bundleTargetPlatform, but the current '
279 280 281 282
      'platform is $targetPlatform. This may lead to less efficient shader '
      'caching.'
    );
  }
283
  return DevFSStringContent(json.encode(<String, Object?>{
284 285 286 287
    'data': bundle['data'],
  }));
}

288 289 290
/// Copy the assets defined in the flutter manifest into a build directory.
class CopyAssets extends Target {
  const CopyAssets();
291

292 293 294 295
  @override
  String get name => 'copy_assets';

  @override
296 297 298
  List<Target> get dependencies => const <Target>[
    KernelSnapshot(),
  ];
299 300 301 302

  @override
  List<Source> get inputs => const <Source>[
    Source.pattern('{FLUTTER_ROOT}/packages/flutter_tools/lib/src/build_system/targets/assets.dart'),
303
    ...IconTreeShaker.inputs,
304
    ...ShaderCompiler.inputs,
305 306 307
  ];

  @override
308 309 310 311
  List<Source> get outputs => const <Source>[];

  @override
  List<String> get depfiles => const <String>[
312
    'flutter_assets.d',
313 314 315
  ];

  @override
316
  Future<void> build(Environment environment) async {
317 318 319 320
    final Directory output = environment
      .buildDir
      .childDirectory('flutter_assets');
    output.createSync(recursive: true);
321 322 323 324
    final Depfile depfile = await copyAssets(
      environment,
      output,
      targetPlatform: TargetPlatform.android,
325
      shaderTarget: ShaderTarget.sksl,
326
    );
327
    environment.depFileService.writeToFile(
328 329 330
      depfile,
      environment.buildDir.childFile('flutter_assets.d'),
    );
331 332
  }
}