flx.dart 14.1 KB
Newer Older
1 2 3 4 5
// Copyright 2015 The Chromium 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 'dart:async';
6
import 'dart:convert';
7 8 9 10
import 'dart:io';

import 'package:flx/bundle.dart';
import 'package:flx/signing.dart';
11
import 'package:json_schema/json_schema.dart';
12 13 14
import 'package:path/path.dart' as path;
import 'package:yaml/yaml.dart';

yjbanov's avatar
yjbanov committed
15
import 'base/file_system.dart' show ensureDirectoryExists;
16
import 'base/process.dart';
17
import 'cache.dart';
18
import 'globals.dart';
19
import 'package_map.dart';
20
import 'toolchain.dart';
Devon Carew's avatar
Devon Carew committed
21
import 'zip.dart';
22 23

const String defaultMainPath = 'lib/main.dart';
24
const String defaultAssetBasePath = '.';
25 26 27
const String defaultManifestPath = 'flutter.yaml';
const String defaultFlxOutputPath = 'build/app.flx';
const String defaultSnapshotPath = 'build/snapshot_blob.bin';
28
const String defaultDepfilePath = 'build/snapshot_blob.bin.d';
29
const String defaultPrivateKeyPath = 'privatekey.der';
30
const String defaultWorkingDirPath = 'build/flx';
31 32 33

const String _kSnapshotKey = 'snapshot_blob.bin';

34 35 36
const String _kFontSetMaterial = 'material';
const String _kFontSetRoboto = 'roboto';

37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
Future<int> createSnapshot({
  String mainPath,
  String snapshotPath,
  String depfilePath,
  String buildOutputPath
}) {
  assert(mainPath != null);
  assert(snapshotPath != null);

  final List<String> args = <String>[
    tools.getHostToolPath(HostTool.SkySnapshot),
    mainPath,
    '--packages=${PackageMap.instance.packagesPath}',
    '--snapshot=$snapshotPath'
  ];
  if (depfilePath != null)
    args.add('--depfile=$depfilePath');
  if (buildOutputPath != null)
    args.add('--build-output=$buildOutputPath');
  return runCommandAndStreamOutput(args);
}

59
class _Asset {
Devon Carew's avatar
Devon Carew committed
60 61 62 63 64
  _Asset({ this.base, String assetEntry, this.relativePath, this.source }) {
    this._assetEntry = assetEntry;
  }

  String _assetEntry;
Devon Carew's avatar
Devon Carew committed
65

66
  final String base;
Devon Carew's avatar
Devon Carew committed
67 68 69 70 71 72 73 74 75

  /// The entry to list in the generated asset manifest.
  String get assetEntry => _assetEntry ?? relativePath;

  /// Where the resource is on disk realtive to [base].
  final String relativePath;

  final String source;

76 77 78 79 80 81
  File get assetFile {
    return new File(source != null ? '$base/$source' : '$base/$relativePath');
  }

  bool get assetFileExists => assetFile.existsSync();

Devon Carew's avatar
Devon Carew committed
82
  /// The delta between what the assetEntry is and the relativePath (e.g.,
83
  /// packages/flutter_gallery).
Devon Carew's avatar
Devon Carew committed
84 85 86 87 88 89
  String get symbolicPrefix {
    if (_assetEntry == null || _assetEntry == relativePath)
      return null;
    int index = _assetEntry.indexOf(relativePath);
    return index == -1 ? null : _assetEntry.substring(0, index);
  }
90 91

  @override
Devon Carew's avatar
Devon Carew committed
92
  String toString() => 'asset: $assetEntry';
93 94
}

95
Map<String, dynamic> _readMaterialFontsManifest() {
96
  String fontsPath = path.join(path.absolute(Cache.flutterRoot),
97
      'packages', 'flutter_tools', 'schema', 'material_fonts.yaml');
98

99
  return loadYaml(new File(fontsPath).readAsStringSync());
100 101
}

102 103 104 105 106 107 108 109 110 111 112 113 114
final Map<String, dynamic> _materialFontsManifest = _readMaterialFontsManifest();

List<Map<String, dynamic>> _getMaterialFonts(String fontSet) {
  return _materialFontsManifest[fontSet];
}

List<_Asset> _getMaterialAssets(String fontSet) {
  List<_Asset> result = <_Asset>[];

  for (Map<String, dynamic> family in _getMaterialFonts(fontSet)) {
    for (Map<String, dynamic> font in family['fonts']) {
      String assetKey = font['asset'];
      result.add(new _Asset(
115
        base: '${Cache.flutterRoot}/bin/cache/artifacts/material_fonts',
116
        source: path.basename(assetKey),
Devon Carew's avatar
Devon Carew committed
117
        relativePath: assetKey
118 119 120 121 122
      ));
    }
  }

  return result;
123 124
}

125 126
/// Given an assetBase location and a flutter.yaml manifest, return a map of
/// assets to asset variants.
127 128
///
/// Returns `null` on missing assets.
129 130 131
Map<_Asset, List<_Asset>> _parseAssets(
  PackageMap packageMap,
  Map<String, dynamic> manifestDescriptor,
132 133 134
  String assetBase, {
  List<String> excludeDirs: const <String>[]
}) {
135
  Map<_Asset, List<_Asset>> result = <_Asset, List<_Asset>>{};
136

137 138
  if (manifestDescriptor == null)
    return result;
139 140 141 142

  excludeDirs = excludeDirs.map(
    (String exclude) => path.absolute(exclude) + Platform.pathSeparator).toList();

143 144
  if (manifestDescriptor.containsKey('assets')) {
    for (String asset in manifestDescriptor['assets']) {
145 146
      _Asset baseAsset = _resolveAsset(packageMap, assetBase, asset);

147 148 149 150 151
      if (!baseAsset.assetFileExists) {
        printError('Error: unable to locate asset entry in flutter.yaml: "$asset".');
        return null;
      }

152 153
      List<_Asset> variants = <_Asset>[];
      result[baseAsset] = variants;
154

155
      // Find asset variants
Devon Carew's avatar
Devon Carew committed
156
      String assetPath = path.join(baseAsset.base, baseAsset.relativePath);
157 158
      String assetFilename = path.basename(assetPath);
      Directory assetDir = new Directory(path.dirname(assetPath));
159

160
      List<FileSystemEntity> files = assetDir.listSync(recursive: true);
161

162
      for (FileSystemEntity entity in files) {
163 164 165 166 167 168 169 170
        if (!FileSystemEntity.isFileSync(entity.path))
          continue;

        // Exclude any files in the given directories.
        if (excludeDirs.any((String exclude) => entity.path.startsWith(exclude)))
          continue;

        if (path.basename(entity.path) == assetFilename && entity.path != assetPath) {
171
          String key = path.relative(entity.path, from: baseAsset.base);
Devon Carew's avatar
Devon Carew committed
172 173 174 175
          String assetEntry;
          if (baseAsset.symbolicPrefix != null)
            assetEntry = path.join(baseAsset.symbolicPrefix, key);
          variants.add(new _Asset(base: baseAsset.base, assetEntry: assetEntry, relativePath: key));
176 177 178 179
        }
      }
    }
  }
180 181 182 183 184 185 186 187 188 189 190

  // Add assets referenced in the fonts section of the manifest.
  if (manifestDescriptor.containsKey('fonts')) {
    for (Map<String, dynamic> family in manifestDescriptor['fonts']) {
      List<Map<String, dynamic>> fonts = family['fonts'];
      if (fonts == null) continue;

      for (Map<String, dynamic> font in fonts) {
        String asset = font['asset'];
        if (asset == null) continue;

Devon Carew's avatar
Devon Carew committed
191
        _Asset baseAsset = new _Asset(base: assetBase, relativePath: asset);
192 193 194 195 196
        result[baseAsset] = <_Asset>[];
      }
    }
  }

197
  return result;
198 199
}

200 201 202 203 204 205 206 207 208 209 210 211 212 213 214
_Asset _resolveAsset(PackageMap packageMap, String assetBase, String asset) {
  if (asset.startsWith('packages/')) {
    // Convert packages/flutter_gallery_assets/clouds-0.png to clouds-0.png.
    String packageKey = asset.substring(9);
    String relativeAsset = asset;

    int index = packageKey.indexOf('/');
    if (index != -1) {
      relativeAsset = packageKey.substring(index + 1);
      packageKey = packageKey.substring(0, index);
    }

    Uri uri = packageMap.map[packageKey];
    if (uri != null && uri.scheme == 'file') {
      File file = new File.fromUri(uri);
Devon Carew's avatar
Devon Carew committed
215
      return new _Asset(base: file.path, assetEntry: asset, relativePath: relativeAsset);
216 217 218
    }
  }

Devon Carew's avatar
Devon Carew committed
219
  return new _Asset(base: assetBase, relativePath: asset);
220 221
}

222 223 224 225 226 227 228
dynamic _loadManifest(String manifestPath) {
  if (manifestPath == null || !FileSystemEntity.isFileSync(manifestPath))
    return null;
  String manifestDescriptor = new File(manifestPath).readAsStringSync();
  return loadYaml(manifestDescriptor);
}

229
Future<int> _validateManifest(Object manifest) async {
230
  String schemaPath = path.join(path.absolute(Cache.flutterRoot),
231 232 233 234
      'packages', 'flutter_tools', 'schema', 'flutter_yaml.json');
  Schema schema = await Schema.createSchemaFromUrl('file://$schemaPath');

  Validator validator = new Validator(schema);
235
  if (validator.validate(manifest)) {
236
    return 0;
237 238 239 240 241 242 243
  } else {
    if (validator.errors.length == 1) {
      printError('Error in flutter.yaml: ${validator.errors.first}');
    } else {
      printError('Error in flutter.yaml:');
      printError('  ' + validator.errors.join('\n  '));
    }
244

245 246
    return 1;
  }
247 248
}

249
/// Create a [ZipEntry] from the given [_Asset]; the asset must exist.
Devon Carew's avatar
Devon Carew committed
250
ZipEntry _createAssetEntry(_Asset asset) {
251 252
  assert(asset.assetFileExists);
  return new ZipEntry.fromFile(asset.assetEntry, asset.assetFile);
253 254
}

255
ZipEntry _createAssetManifest(Map<_Asset, List<_Asset>> assetVariants) {
256
  Map<String, List<String>> json = <String, List<String>>{};
257
  for (_Asset main in assetVariants.keys) {
258
    List<String> variants = <String>[];
259
    for (_Asset variant in assetVariants[main])
Devon Carew's avatar
Devon Carew committed
260 261
      variants.add(variant.relativePath);
    json[main.relativePath] = variants;
262
  }
Devon Carew's avatar
Devon Carew committed
263
  return new ZipEntry.fromString('AssetManifest.json', JSON.encode(json));
264 265
}

266 267 268
ZipEntry _createFontManifest(Map<String, dynamic> manifestDescriptor,
                             bool usesMaterialDesign,
                             bool includeRobotoFonts) {
Ian Hickson's avatar
Ian Hickson committed
269
  List<Map<String, dynamic>> fonts = <Map<String, dynamic>>[];
270 271 272 273 274
  if (usesMaterialDesign) {
    fonts.addAll(_getMaterialFonts(_kFontSetMaterial));
    if (includeRobotoFonts)
      fonts.addAll(_getMaterialFonts(_kFontSetRoboto));
  }
275 276 277
  if (manifestDescriptor != null && manifestDescriptor.containsKey('fonts'))
    fonts.addAll(manifestDescriptor['fonts']);
  if (fonts.isEmpty)
278
    return null;
279
  return new ZipEntry.fromString('FontManifest.json', JSON.encode(fonts));
280 281
}

Devon Carew's avatar
Devon Carew committed
282
/// Build the flx in the build/ directory and return `localBundlePath` on success.
283 284
///
/// Return `null` on failure.
285
Future<String> buildFlx({
286
  String mainPath: defaultMainPath,
287
  bool precompiledSnapshot: false,
288
  bool includeRobotoFonts: true
289 290
}) async {
  int result;
Devon Carew's avatar
Devon Carew committed
291 292
  String localBundlePath = path.join('build', 'app.flx');
  String localSnapshotPath = path.join('build', 'snapshot_blob.bin');
293 294 295
  result = await build(
    snapshotPath: localSnapshotPath,
    outputPath: localBundlePath,
296
    mainPath: mainPath,
297
    precompiledSnapshot: precompiledSnapshot,
298
    includeRobotoFonts: includeRobotoFonts
299
  );
300
  return result == 0 ? localBundlePath : null;
301 302 303 304
}

/// The result from [buildInTempDir]. Note that this object should be disposed after use.
class DirectoryResult {
Devon Carew's avatar
Devon Carew committed
305 306
  DirectoryResult(this.directory, this.localBundlePath);

307 308 309 310 311 312
  final Directory directory;
  final String localBundlePath;

  /// Call this to delete the temporary directory.
  void dispose() {
    directory.deleteSync(recursive: true);
313 314 315
  }
}

316
Future<int> build({
317 318 319 320
  String mainPath: defaultMainPath,
  String manifestPath: defaultManifestPath,
  String outputPath: defaultFlxOutputPath,
  String snapshotPath: defaultSnapshotPath,
321
  String depfilePath: defaultDepfilePath,
322
  String privateKeyPath: defaultPrivateKeyPath,
323
  String workingDirPath: defaultWorkingDirPath,
324 325
  bool precompiledSnapshot: false,
  bool includeRobotoFonts: true
326
}) async {
327 328 329 330 331 332 333 334
  Object manifest = _loadManifest(manifestPath);
  if (manifest != null) {
    int result = await _validateManifest(manifest);
    if (result != 0)
      return result;
  }
  Map<String, dynamic> manifestDescriptor = manifest;

335
  String assetBasePath = path.dirname(path.absolute(manifestPath));
336

Devon Carew's avatar
Devon Carew committed
337 338
  File snapshotFile;

339 340 341 342 343
  if (!precompiledSnapshot) {
    ensureDirectoryExists(snapshotPath);

    // In a precompiled snapshot, the instruction buffer contains script
    // content equivalents
344
    int result = await createSnapshot(
345 346 347 348
      mainPath: mainPath,
      snapshotPath: snapshotPath,
      depfilePath: depfilePath
    );
349
    if (result != 0) {
350
      printError('Failed to run the Flutter compiler. Exit code: $result');
351 352 353
      return result;
    }

Devon Carew's avatar
Devon Carew committed
354
    snapshotFile = new File(snapshotPath);
355 356
  }

357 358 359 360 361
  return assemble(
      manifestDescriptor: manifestDescriptor,
      snapshotFile: snapshotFile,
      assetBasePath: assetBasePath,
      outputPath: outputPath,
362
      privateKeyPath: privateKeyPath,
363 364
      workingDirPath: workingDirPath,
      includeRobotoFonts: includeRobotoFonts
365 366 367 368
  );
}

Future<int> assemble({
Ian Hickson's avatar
Ian Hickson committed
369
  Map<String, dynamic> manifestDescriptor: const <String, dynamic>{},
Devon Carew's avatar
Devon Carew committed
370
  File snapshotFile,
371 372
  String assetBasePath: defaultAssetBasePath,
  String outputPath: defaultFlxOutputPath,
373
  String privateKeyPath: defaultPrivateKeyPath,
374 375
  String workingDirPath: defaultWorkingDirPath,
  bool includeRobotoFonts: true
376 377 378
}) async {
  printTrace('Building $outputPath');

379 380 381 382 383 384
  Map<_Asset, List<_Asset>> assetVariants = _parseAssets(
    new PackageMap(path.join(assetBasePath, '.packages')),
    manifestDescriptor,
    assetBasePath,
    excludeDirs: <String>[workingDirPath, path.join(assetBasePath, 'build')]
  );
385

386 387 388
  if (assetVariants == null)
    return 1;

389 390
  final bool usesMaterialDesign = manifestDescriptor != null &&
    manifestDescriptor['uses-material-design'] == true;
391

Devon Carew's avatar
Devon Carew committed
392
  ZipBuilder zipBuilder = new ZipBuilder();
393 394

  if (snapshotFile != null)
Devon Carew's avatar
Devon Carew committed
395
    zipBuilder.addEntry(new ZipEntry.fromFile(_kSnapshotKey, snapshotFile));
396

397
  for (_Asset asset in assetVariants.keys) {
Devon Carew's avatar
Devon Carew committed
398 399
    ZipEntry assetEntry = _createAssetEntry(asset);
    if (assetEntry == null)
400
      return 1;
401
    zipBuilder.addEntry(assetEntry);
Devon Carew's avatar
Devon Carew committed
402

403
    for (_Asset variant in assetVariants[asset]) {
Devon Carew's avatar
Devon Carew committed
404 405
      ZipEntry variantEntry = _createAssetEntry(variant);
      if (variantEntry == null)
406
        return 1;
407 408 409 410
      zipBuilder.addEntry(variantEntry);
    }
  }

411
  List<_Asset> materialAssets = <_Asset>[];
412
  if (usesMaterialDesign) {
413 414 415 416 417 418 419 420 421
    materialAssets.addAll(_getMaterialAssets(_kFontSetMaterial));
    if (includeRobotoFonts)
      materialAssets.addAll(_getMaterialAssets(_kFontSetRoboto));
  }
  for (_Asset asset in materialAssets) {
    ZipEntry assetEntry = _createAssetEntry(asset);
    if (assetEntry == null)
      return 1;
    zipBuilder.addEntry(assetEntry);
422 423
  }

424
  zipBuilder.addEntry(_createAssetManifest(assetVariants));
425

426
  ZipEntry fontManifest = _createFontManifest(manifestDescriptor, usesMaterialDesign, includeRobotoFonts);
427
  if (fontManifest != null)
Devon Carew's avatar
Devon Carew committed
428
    zipBuilder.addEntry(fontManifest);
429

Ian Hickson's avatar
Ian Hickson committed
430
  AsymmetricKeyPair<PublicKey, PrivateKey> keyPair = keyPairFromPrivateKeyFileSync(privateKeyPath);
431
  printTrace('KeyPair from $privateKeyPath: $keyPair.');
432 433 434 435 436 437

  if (keyPair != null) {
    printTrace('Calling CipherParameters.seedRandom().');
    CipherParameters.get().seedRandom();
  }

Devon Carew's avatar
Devon Carew committed
438 439
  File zipFile = new File(outputPath.substring(0, outputPath.length - 4) + '.zip');
  printTrace('Encoding zip file to ${zipFile.path}');
440
  zipBuilder.createZip(zipFile, new Directory(workingDirPath));
Devon Carew's avatar
Devon Carew committed
441
  List<int> zipBytes = zipFile.readAsBytesSync();
442

443
  ensureDirectoryExists(outputPath);
444

Devon Carew's avatar
Devon Carew committed
445
  printTrace('Creating flx at $outputPath.');
446 447 448 449 450 451 452
  Bundle bundle = new Bundle.fromContent(
    path: outputPath,
    manifest: manifestDescriptor,
    contentBytes: zipBytes,
    keyPair: keyPair
  );
  bundle.writeSync();
453

Devon Carew's avatar
Devon Carew committed
454
  printTrace('Built $outputPath.');
455

456 457
  return 0;
}