Support for synchronizing assets onto a DevFS
diff --git a/packages/flutter_tools/lib/src/asset.dart b/packages/flutter_tools/lib/src/asset.dart
new file mode 100644
index 0000000..d681ca7
--- /dev/null
+++ b/packages/flutter_tools/lib/src/asset.dart
@@ -0,0 +1,418 @@
+// Copyright 2016 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';
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:json_schema/json_schema.dart';
+import 'package:path/path.dart' as path;
+import 'package:yaml/yaml.dart';
+
+import 'cache.dart';
+import 'dart/package_map.dart';
+import 'globals.dart';
+
+/// An entry in an asset bundle.
+class AssetBundleEntry {
+  /// An entry backed by a File.
+  AssetBundleEntry.fromFile(this.archivePath, this.file)
+      : _contents = null;
+
+  /// An entry backed by a String.
+  AssetBundleEntry.fromString(this.archivePath, this._contents)
+      : file = null;
+
+  /// The path within the bundle.
+  final String archivePath;
+
+  /// The payload.
+  List<int> contentsAsBytes() {
+    if (_contents != null) {
+      return UTF8.encode(_contents);
+    } else {
+      return file.readAsBytesSync();
+    }
+  }
+
+  bool get isStringEntry => _contents != null;
+
+  final File file;
+  final String _contents;
+}
+
+/// A bundle of assets.
+class AssetBundle {
+  final Set<AssetBundleEntry> entries = new Set<AssetBundleEntry>();
+
+  static const String defaultManifestPath = 'flutter.yaml';
+  static const String defaultWorkingDirPath = 'build/flx';
+  static const String _kFontSetMaterial = 'material';
+  static const String _kFontSetRoboto = 'roboto';
+
+  Future<int> build({String manifestPath: defaultManifestPath,
+                     String workingDirPath: defaultWorkingDirPath,
+                     bool includeRobotoFonts: true}) async {
+    Object manifest = _loadFlutterYamlManifest(manifestPath);
+    if (manifest != null) {
+     int result = await _validateFlutterYamlManifest(manifest);
+     if (result != 0)
+       return result;
+    }
+    Map<String, dynamic> manifestDescriptor = manifest;
+    assert(manifestDescriptor != null);
+    String assetBasePath = path.dirname(path.absolute(manifestPath));
+
+    final PackageMap packageMap =
+        new PackageMap(path.join(assetBasePath, '.packages'));
+
+    Map<_Asset, List<_Asset>> assetVariants = _parseAssets(
+      packageMap,
+      manifestDescriptor,
+      assetBasePath,
+      excludeDirs: <String>[workingDirPath, path.join(assetBasePath, 'build')]
+    );
+
+    if (assetVariants == null)
+      return 1;
+
+    final bool usesMaterialDesign = (manifestDescriptor != null) &&
+        manifestDescriptor['uses-material-design'];
+
+    for (_Asset asset in assetVariants.keys) {
+      AssetBundleEntry assetEntry = _createAssetEntry(asset);
+      if (assetEntry == null)
+        return 1;
+      entries.add(assetEntry);
+
+      for (_Asset variant in assetVariants[asset]) {
+        AssetBundleEntry variantEntry = _createAssetEntry(variant);
+        if (variantEntry == null)
+          return 1;
+        entries.add(variantEntry);
+      }
+    }
+
+    List<_Asset> materialAssets = <_Asset>[];
+    if (usesMaterialDesign) {
+      materialAssets.addAll(_getMaterialAssets(_kFontSetMaterial));
+      if (includeRobotoFonts)
+        materialAssets.addAll(_getMaterialAssets(_kFontSetRoboto));
+    }
+    for (_Asset asset in materialAssets) {
+      AssetBundleEntry assetEntry = _createAssetEntry(asset);
+      if (assetEntry == null)
+        return 1;
+      entries.add(assetEntry);
+    }
+
+    entries.add(_createAssetManifest(assetVariants));
+
+    AssetBundleEntry fontManifest =
+        _createFontManifest(manifestDescriptor, usesMaterialDesign, includeRobotoFonts);
+    if (fontManifest != null)
+      entries.add(fontManifest);
+
+    // TODO(ianh): Only do the following line if we've changed packages
+    entries.add(await _obtainLicenses(packageMap, assetBasePath));
+
+    return 0;
+  }
+
+  void dump() {
+    print('Dumping AssetBundle:');
+    for (AssetBundleEntry entry in entries) {
+      print(entry.archivePath);
+    }
+  }
+}
+
+class _Asset {
+  _Asset({ this.base, String assetEntry, this.relativePath, this.source }) {
+    this._assetEntry = assetEntry;
+  }
+
+  String _assetEntry;
+
+  final String base;
+
+  /// The entry to list in the generated asset manifest.
+  String get assetEntry => _assetEntry ?? relativePath;
+
+  /// Where the resource is on disk relative to [base].
+  final String relativePath;
+
+  final String source;
+
+  File get assetFile {
+    return new File(source != null ? '$base/$source' : '$base/$relativePath');
+  }
+
+  bool get assetFileExists => assetFile.existsSync();
+
+  /// The delta between what the assetEntry is and the relativePath (e.g.,
+  /// packages/flutter_gallery).
+  String get symbolicPrefix {
+    if (_assetEntry == null || _assetEntry == relativePath)
+      return null;
+    int index = _assetEntry.indexOf(relativePath);
+    return index == -1 ? null : _assetEntry.substring(0, index);
+  }
+
+  @override
+  String toString() => 'asset: $assetEntry';
+}
+
+Map<String, dynamic> _readMaterialFontsManifest() {
+  String fontsPath = path.join(path.absolute(Cache.flutterRoot),
+      'packages', 'flutter_tools', 'schema', 'material_fonts.yaml');
+
+  return loadYaml(new File(fontsPath).readAsStringSync());
+}
+
+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(
+        base: '${Cache.flutterRoot}/bin/cache/artifacts/material_fonts',
+        source: path.basename(assetKey),
+        relativePath: assetKey
+      ));
+    }
+  }
+
+  return result;
+}
+
+final String _licenseSeparator = '\n' + ('-' * 80) + '\n';
+
+/// Returns a AssetBundleEntry representing the license file.
+Future<AssetBundleEntry> _obtainLicenses(
+  PackageMap packageMap,
+  String assetBase
+) async {
+  // Read the LICENSE file from each package in the .packages file,
+  // splitting each one into each component license (so that we can
+  // de-dupe if possible).
+  // For the sky_engine package we assume each license starts with
+  // package names. For the other packages we assume that each
+  // license is raw.
+  final Map<String, Set<String>> packageLicenses = <String, Set<String>>{};
+  for (String packageName in packageMap.map.keys) {
+    final Uri package = packageMap.map[packageName];
+    if (package != null && package.scheme == 'file') {
+      final File file = new File.fromUri(package.resolve('../LICENSE'));
+      if (file.existsSync()) {
+        final List<String> rawLicenses =
+            (await file.readAsString()).split(_licenseSeparator);
+        for (String rawLicense in rawLicenses) {
+          String licenseText;
+          List<String> packageNames;
+          if (packageName == 'sky_engine') {
+            final int split = rawLicense.indexOf('\n\n');
+            if (split >= 0) {
+              packageNames = rawLicense.substring(0, split).split('\n');
+              licenseText = rawLicense.substring(split + 2);
+            }
+          }
+          if (licenseText == null) {
+            licenseText = rawLicense;
+            packageNames = <String>[packageName];
+          }
+          packageLicenses.putIfAbsent(rawLicense, () => new Set<String>())
+            ..addAll(packageNames);
+        }
+      }
+    }
+  }
+
+  final List<String> combinedLicensesList = packageLicenses.keys.map(
+    (String license) {
+      List<String> packageNames = packageLicenses[license].toList()
+       ..sort();
+      return packageNames.join('\n') + '\n\n' + license;
+    }
+  ).toList();
+  combinedLicensesList.sort();
+
+  final String combinedLicenses = combinedLicensesList.join(_licenseSeparator);
+
+  return new AssetBundleEntry.fromString('LICENSE', combinedLicenses);
+}
+
+
+/// Create a [AssetBundleEntry] from the given [_Asset]; the asset must exist.
+AssetBundleEntry _createAssetEntry(_Asset asset) {
+  assert(asset.assetFileExists);
+  return new AssetBundleEntry.fromFile(asset.assetEntry, asset.assetFile);
+}
+
+AssetBundleEntry _createAssetManifest(Map<_Asset, List<_Asset>> assetVariants) {
+  Map<String, List<String>> json = <String, List<String>>{};
+  for (_Asset main in assetVariants.keys) {
+    List<String> variants = <String>[];
+    for (_Asset variant in assetVariants[main])
+      variants.add(variant.relativePath);
+    json[main.relativePath] = variants;
+  }
+  return new AssetBundleEntry.fromString('AssetManifest.json', JSON.encode(json));
+}
+
+AssetBundleEntry _createFontManifest(Map<String, dynamic> manifestDescriptor,
+                             bool usesMaterialDesign,
+                             bool includeRobotoFonts) {
+  List<Map<String, dynamic>> fonts = <Map<String, dynamic>>[];
+  if (usesMaterialDesign) {
+    fonts.addAll(_getMaterialFonts(AssetBundle._kFontSetMaterial));
+    if (includeRobotoFonts)
+      fonts.addAll(_getMaterialFonts(AssetBundle._kFontSetRoboto));
+  }
+  if (manifestDescriptor != null && manifestDescriptor.containsKey('fonts'))
+    fonts.addAll(manifestDescriptor['fonts']);
+  if (fonts.isEmpty)
+    return null;
+  return new AssetBundleEntry.fromString('FontManifest.json', JSON.encode(fonts));
+}
+
+/// Given an assetBase location and a flutter.yaml manifest, return a map of
+/// assets to asset variants.
+///
+/// Returns `null` on missing assets.
+Map<_Asset, List<_Asset>> _parseAssets(
+  PackageMap packageMap,
+  Map<String, dynamic> manifestDescriptor,
+  String assetBase, {
+  List<String> excludeDirs: const <String>[]
+}) {
+  Map<_Asset, List<_Asset>> result = <_Asset, List<_Asset>>{};
+
+  if (manifestDescriptor == null)
+    return result;
+
+  excludeDirs = excludeDirs.map(
+    (String exclude) => path.absolute(exclude) + Platform.pathSeparator).toList();
+
+  if (manifestDescriptor.containsKey('assets')) {
+    for (String asset in manifestDescriptor['assets']) {
+      _Asset baseAsset = _resolveAsset(packageMap, assetBase, asset);
+
+      if (!baseAsset.assetFileExists) {
+        printError('Error: unable to locate asset entry in flutter.yaml: "$asset".');
+        return null;
+      }
+
+      List<_Asset> variants = <_Asset>[];
+      result[baseAsset] = variants;
+
+      // Find asset variants
+      String assetPath = baseAsset.assetFile.path;
+      String assetFilename = path.basename(assetPath);
+      Directory assetDir = new Directory(path.dirname(assetPath));
+
+      List<FileSystemEntity> files = assetDir.listSync(recursive: true);
+
+      for (FileSystemEntity entity in files) {
+        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) {
+          String key = path.relative(entity.path, from: baseAsset.base);
+          String assetEntry;
+          if (baseAsset.symbolicPrefix != null)
+            assetEntry = path.join(baseAsset.symbolicPrefix, key);
+          variants.add(new _Asset(base: baseAsset.base, assetEntry: assetEntry, relativePath: key));
+        }
+      }
+    }
+  }
+
+  // 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;
+
+        _Asset baseAsset = _resolveAsset(packageMap, assetBase, asset);
+        if (!baseAsset.assetFileExists) {
+          printError('Error: unable to locate asset entry in flutter.yaml: "$asset".');
+          return null;
+        }
+
+        result[baseAsset] = <_Asset>[];
+      }
+    }
+  }
+
+  return result;
+}
+
+_Asset _resolveAsset(
+  PackageMap packageMap,
+  String assetBase,
+  String asset
+) {
+  if (asset.startsWith('packages/') && !FileSystemEntity.isFileSync(path.join(assetBase, asset))) {
+    // 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);
+      return new _Asset(base: file.path, assetEntry: asset, relativePath: relativeAsset);
+    }
+  }
+
+  return new _Asset(base: assetBase, relativePath: asset);
+}
+
+dynamic _loadFlutterYamlManifest(String manifestPath) {
+  if (manifestPath == null || !FileSystemEntity.isFileSync(manifestPath))
+    return null;
+  String manifestDescriptor = new File(manifestPath).readAsStringSync();
+  return loadYaml(manifestDescriptor);
+}
+
+Future<int> _validateFlutterYamlManifest(Object manifest) async {
+  String schemaPath = path.join(path.absolute(Cache.flutterRoot),
+      'packages', 'flutter_tools', 'schema', 'flutter_yaml.json');
+  Schema schema = await Schema.createSchemaFromUrl('file://$schemaPath');
+
+  Validator validator = new Validator(schema);
+  if (validator.validate(manifest)) {
+    return 0;
+  } 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  '));
+    }
+
+    return 1;
+  }
+}
diff --git a/packages/flutter_tools/lib/src/devfs.dart b/packages/flutter_tools/lib/src/devfs.dart
index 4ac4df2..de56314 100644
--- a/packages/flutter_tools/lib/src/devfs.dart
+++ b/packages/flutter_tools/lib/src/devfs.dart
@@ -9,23 +9,37 @@
 import 'package:path/path.dart' as path;
 
 import 'dart/package_map.dart';
+import 'asset.dart';
 import 'globals.dart';
 import 'observatory.dart';
 
 // A file that has been added to a DevFS.
 class DevFSEntry {
-  DevFSEntry(this.devicePath, this.file);
+  DevFSEntry(this.devicePath, this.file)
+      : bundleEntry = null;
+
+  DevFSEntry.bundle(this.devicePath, AssetBundleEntry bundleEntry)
+      : bundleEntry = bundleEntry,
+        file = bundleEntry.file;
 
   final String devicePath;
+  final AssetBundleEntry bundleEntry;
+
   final File file;
   FileStat _fileStat;
-
+  // When we updated the DevFS, did we see this entry?
+  bool _wasSeen = false;
   DateTime get lastModified => _fileStat?.modified;
   bool get stillExists {
+    if (_isSourceEntry)
+      return true;
     _stat();
     return _fileStat.type != FileSystemEntityType.NOT_FOUND;
   }
   bool get isModified {
+    if (_isSourceEntry)
+      return true;
+
     if (_fileStat == null) {
       _stat();
       return true;
@@ -36,8 +50,18 @@
   }
 
   void _stat() {
+    if (_isSourceEntry)
+      return;
     _fileStat = file.statSync();
   }
+
+  bool get _isSourceEntry => file == null;
+
+  Future<List<int>> contentsAsBytes() async {
+    if (_isSourceEntry)
+      return bundleEntry.contentsAsBytes();
+    return file.readAsBytes();
+  }
 }
 
 
@@ -46,6 +70,7 @@
   Future<Uri> create(String fsName);
   Future<dynamic> destroy(String fsName);
   Future<dynamic> writeFile(String fsName, DevFSEntry entry);
+  Future<dynamic> deleteFile(String fsName, DevFSEntry entry);
   Future<dynamic> writeSource(String fsName,
                               String devicePath,
                               String contents);
@@ -74,7 +99,7 @@
   Future<dynamic> writeFile(String fsName, DevFSEntry entry) async {
     List<int> bytes;
     try {
-      bytes = await entry.file.readAsBytes();
+      bytes = await entry.contentsAsBytes();
     } catch (e) {
       return e;
     }
@@ -92,6 +117,11 @@
   }
 
   @override
+  Future<dynamic> deleteFile(String fsName, DevFSEntry entry) async {
+    // TODO(johnmccutchan): Add file deletion to the devFS protocol.
+  }
+
+  @override
   Future<dynamic> writeSource(String fsName,
                               String devicePath,
                               String contents) async {
@@ -135,7 +165,11 @@
     return await _operations.destroy(fsName);
   }
 
-  Future<dynamic> update() async {
+  Future<dynamic> update([AssetBundle bundle = null]) async {
+    // Mark all entries as not seen.
+    _entries.forEach((String path, DevFSEntry entry) {
+      entry._wasSeen = false;
+    });
     printTrace('DevFS: Starting sync from $rootDirectory');
     // Send the root and lib directories.
     Directory directory = rootDirectory;
@@ -162,6 +196,27 @@
         }
       }
     }
+    if (bundle != null) {
+      // Synchronize asset bundle.
+      for (AssetBundleEntry entry in bundle.entries) {
+        // We write the assets into 'build/flx' so that they are in the
+        // same location in DevFS and the iOS simulator.
+        final String devicePath = path.join('build/flx', entry.archivePath);
+        _syncBundleEntry(devicePath, entry);
+      }
+    }
+    // Handle deletions.
+    final List<String> toRemove = new List<String>();
+    _entries.forEach((String path, DevFSEntry entry) {
+      if (!entry._wasSeen) {
+        _deleteEntry(path, entry);
+        toRemove.add(path);
+      }
+    });
+    for (int i = 0; i < toRemove.length; i++) {
+      _entries.remove(toRemove[i]);
+    }
+    // Send the assets.
     printTrace('DevFS: Waiting for sync of ${_pendingWrites.length} files '
                'to finish');
     await Future.wait(_pendingWrites);
@@ -175,6 +230,10 @@
     logger.flush();
   }
 
+  void _deleteEntry(String path, DevFSEntry entry) {
+    _pendingWrites.add(_operations.deleteFile(fsName, entry));
+  }
+
   void _syncFile(String devicePath, File file) {
     DevFSEntry entry = _entries[devicePath];
     if (entry == null) {
@@ -182,6 +241,7 @@
       entry = new DevFSEntry(devicePath, file);
       _entries[devicePath] = entry;
     }
+    entry._wasSeen = true;
     bool needsWrite = entry.isModified;
     if (needsWrite) {
       Future<dynamic> pendingWrite = _operations.writeFile(fsName, entry);
@@ -193,13 +253,29 @@
     }
   }
 
-  bool _shouldIgnore(String path) {
+  void _syncBundleEntry(String devicePath, AssetBundleEntry assetBundleEntry) {
+    DevFSEntry entry = _entries[devicePath];
+    if (entry == null) {
+      // New file.
+      entry = new DevFSEntry.bundle(devicePath, assetBundleEntry);
+      _entries[devicePath] = entry;
+    }
+    entry._wasSeen = true;
+    Future<dynamic> pendingWrite = _operations.writeFile(fsName, entry);
+    if (pendingWrite != null) {
+      _pendingWrites.add(pendingWrite);
+    } else {
+      printTrace('DevFS: Failed to sync "$devicePath"');
+    }
+  }
+
+  bool _shouldIgnore(String devicePath) {
     List<String> ignoredPrefixes = <String>['android/',
                                             'build/',
                                             'ios/',
                                             'packages/analyzer'];
     for (String ignoredPrefix in ignoredPrefixes) {
-      if (path.startsWith(ignoredPrefix))
+      if (devicePath.startsWith(ignoredPrefix))
         return true;
     }
     return false;
diff --git a/packages/flutter_tools/lib/src/flx.dart b/packages/flutter_tools/lib/src/flx.dart
index f9ac4b1..7c052be 100644
--- a/packages/flutter_tools/lib/src/flx.dart
+++ b/packages/flutter_tools/lib/src/flx.dart
@@ -3,16 +3,13 @@
 // found in the LICENSE file.
 
 import 'dart:async';
-import 'dart:convert';
 import 'dart:io';
 
-import 'package:json_schema/json_schema.dart';
 import 'package:path/path.dart' as path;
-import 'package:yaml/yaml.dart';
 
+import 'asset.dart';
 import 'base/file_system.dart' show ensureDirectoryExists;
 import 'base/process.dart';
-import 'cache.dart';
 import 'dart/package_map.dart';
 import 'globals.dart';
 import 'toolchain.dart';
@@ -29,9 +26,6 @@
 
 const String _kSnapshotKey = 'snapshot_blob.bin';
 
-const String _kFontSetMaterial = 'material';
-const String _kFontSetRoboto = 'roboto';
-
 Future<int> createSnapshot({
   String mainPath,
   String snapshotPath,
@@ -54,293 +48,6 @@
   return runCommandAndStreamOutput(args);
 }
 
-class _Asset {
-  _Asset({ this.base, String assetEntry, this.relativePath, this.source }) {
-    this._assetEntry = assetEntry;
-  }
-
-  String _assetEntry;
-
-  final String base;
-
-  /// The entry to list in the generated asset manifest.
-  String get assetEntry => _assetEntry ?? relativePath;
-
-  /// Where the resource is on disk relative to [base].
-  final String relativePath;
-
-  final String source;
-
-  File get assetFile {
-    return new File(source != null ? '$base/$source' : '$base/$relativePath');
-  }
-
-  bool get assetFileExists => assetFile.existsSync();
-
-  /// The delta between what the assetEntry is and the relativePath (e.g.,
-  /// packages/flutter_gallery).
-  String get symbolicPrefix {
-    if (_assetEntry == null || _assetEntry == relativePath)
-      return null;
-    int index = _assetEntry.indexOf(relativePath);
-    return index == -1 ? null : _assetEntry.substring(0, index);
-  }
-
-  @override
-  String toString() => 'asset: $assetEntry';
-}
-
-Map<String, dynamic> _readMaterialFontsManifest() {
-  String fontsPath = path.join(path.absolute(Cache.flutterRoot),
-      'packages', 'flutter_tools', 'schema', 'material_fonts.yaml');
-
-  return loadYaml(new File(fontsPath).readAsStringSync());
-}
-
-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(
-        base: '${Cache.flutterRoot}/bin/cache/artifacts/material_fonts',
-        source: path.basename(assetKey),
-        relativePath: assetKey
-      ));
-    }
-  }
-
-  return result;
-}
-
-/// Given an assetBase location and a flutter.yaml manifest, return a map of
-/// assets to asset variants.
-///
-/// Returns `null` on missing assets.
-Map<_Asset, List<_Asset>> _parseAssets(
-  PackageMap packageMap,
-  Map<String, dynamic> manifestDescriptor,
-  String assetBase, {
-  List<String> excludeDirs: const <String>[]
-}) {
-  Map<_Asset, List<_Asset>> result = <_Asset, List<_Asset>>{};
-
-  if (manifestDescriptor == null)
-    return result;
-
-  excludeDirs = excludeDirs.map(
-    (String exclude) => path.absolute(exclude) + Platform.pathSeparator).toList();
-
-  if (manifestDescriptor.containsKey('assets')) {
-    for (String asset in manifestDescriptor['assets']) {
-      _Asset baseAsset = _resolveAsset(packageMap, assetBase, asset);
-
-      if (!baseAsset.assetFileExists) {
-        printError('Error: unable to locate asset entry in flutter.yaml: "$asset".');
-        return null;
-      }
-
-      List<_Asset> variants = <_Asset>[];
-      result[baseAsset] = variants;
-
-      // Find asset variants
-      String assetPath = baseAsset.assetFile.path;
-      String assetFilename = path.basename(assetPath);
-      Directory assetDir = new Directory(path.dirname(assetPath));
-
-      List<FileSystemEntity> files = assetDir.listSync(recursive: true);
-
-      for (FileSystemEntity entity in files) {
-        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) {
-          String key = path.relative(entity.path, from: baseAsset.base);
-          String assetEntry;
-          if (baseAsset.symbolicPrefix != null)
-            assetEntry = path.join(baseAsset.symbolicPrefix, key);
-          variants.add(new _Asset(base: baseAsset.base, assetEntry: assetEntry, relativePath: key));
-        }
-      }
-    }
-  }
-
-  // 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;
-
-        _Asset baseAsset = _resolveAsset(packageMap, assetBase, asset);
-        if (!baseAsset.assetFileExists) {
-          printError('Error: unable to locate asset entry in flutter.yaml: "$asset".');
-          return null;
-        }
-
-        result[baseAsset] = <_Asset>[];
-      }
-    }
-  }
-
-  return result;
-}
-
-final String _licenseSeparator = '\n' + ('-' * 80) + '\n';
-
-/// Returns a ZipEntry representing the license file.
-Future<ZipEntry> _obtainLicenses(
-  PackageMap packageMap,
-  String assetBase
-) async {
-  // Read the LICENSE file from each package in the .packages file,
-  // splitting each one into each component license (so that we can
-  // de-dupe if possible).
-  // For the sky_engine package we assume each license starts with
-  // package names. For the other packages we assume that each
-  // license is raw.
-  final Map<String, Set<String>> packageLicenses = <String, Set<String>>{};
-  for (String packageName in packageMap.map.keys) {
-    final Uri package = packageMap.map[packageName];
-    if (package != null && package.scheme == 'file') {
-      final File file = new File.fromUri(package.resolve('../LICENSE'));
-      if (file.existsSync()) {
-        final List<String> rawLicenses = (await file.readAsString()).split(_licenseSeparator);
-        for (String rawLicense in rawLicenses) {
-          String licenseText;
-          List<String> packageNames;
-          if (packageName == 'sky_engine') {
-            final int split = rawLicense.indexOf('\n\n');
-            if (split >= 0) {
-              packageNames = rawLicense.substring(0, split).split('\n');
-              licenseText = rawLicense.substring(split + 2);
-            }
-          }
-          if (licenseText == null) {
-            licenseText = rawLicense;
-            packageNames = <String>[packageName];
-          }
-          packageLicenses.putIfAbsent(rawLicense, () => new Set<String>())
-            ..addAll(packageNames);
-        }
-      }
-    }
-  }
-
-  final List<String> combinedLicensesList = packageLicenses.keys.map(
-    (String license) {
-      List<String> packageNames = packageLicenses[license].toList()
-       ..sort();
-      return packageNames.join('\n') + '\n\n' + license;
-    }
-  ).toList();
-  combinedLicensesList.sort();
-
-  final String combinedLicenses = combinedLicensesList.join(_licenseSeparator);
-
-  return new ZipEntry.fromString('LICENSE', combinedLicenses);
-}
-
-_Asset _resolveAsset(
-  PackageMap packageMap,
-  String assetBase,
-  String asset
-) {
-  if (asset.startsWith('packages/') && !FileSystemEntity.isFileSync(path.join(assetBase, asset))) {
-    // 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);
-      return new _Asset(base: file.path, assetEntry: asset, relativePath: relativeAsset);
-    }
-  }
-
-  return new _Asset(base: assetBase, relativePath: asset);
-}
-
-dynamic _loadManifest(String manifestPath) {
-  if (manifestPath == null || !FileSystemEntity.isFileSync(manifestPath))
-    return null;
-  String manifestDescriptor = new File(manifestPath).readAsStringSync();
-  return loadYaml(manifestDescriptor);
-}
-
-Future<int> _validateManifest(Object manifest) async {
-  String schemaPath = path.join(path.absolute(Cache.flutterRoot),
-      'packages', 'flutter_tools', 'schema', 'flutter_yaml.json');
-  Schema schema = await Schema.createSchemaFromUrl('file://$schemaPath');
-
-  Validator validator = new Validator(schema);
-  if (validator.validate(manifest)) {
-    return 0;
-  } 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  '));
-    }
-
-    return 1;
-  }
-}
-
-/// Create a [ZipEntry] from the given [_Asset]; the asset must exist.
-ZipEntry _createAssetEntry(_Asset asset) {
-  assert(asset.assetFileExists);
-  return new ZipEntry.fromFile(asset.assetEntry, asset.assetFile);
-}
-
-ZipEntry _createAssetManifest(Map<_Asset, List<_Asset>> assetVariants) {
-  Map<String, List<String>> json = <String, List<String>>{};
-  for (_Asset main in assetVariants.keys) {
-    List<String> variants = <String>[];
-    for (_Asset variant in assetVariants[main])
-      variants.add(variant.relativePath);
-    json[main.relativePath] = variants;
-  }
-  return new ZipEntry.fromString('AssetManifest.json', JSON.encode(json));
-}
-
-ZipEntry _createFontManifest(Map<String, dynamic> manifestDescriptor,
-                             bool usesMaterialDesign,
-                             bool includeRobotoFonts) {
-  List<Map<String, dynamic>> fonts = <Map<String, dynamic>>[];
-  if (usesMaterialDesign) {
-    fonts.addAll(_getMaterialFonts(_kFontSetMaterial));
-    if (includeRobotoFonts)
-      fonts.addAll(_getMaterialFonts(_kFontSetRoboto));
-  }
-  if (manifestDescriptor != null && manifestDescriptor.containsKey('fonts'))
-    fonts.addAll(manifestDescriptor['fonts']);
-  if (fonts.isEmpty)
-    return null;
-  return new ZipEntry.fromString('FontManifest.json', JSON.encode(fonts));
-}
-
 /// Build the flx in the build/ directory and return `localBundlePath` on success.
 ///
 /// Return `null` on failure.
@@ -362,19 +69,6 @@
   return result == 0 ? localBundlePath : null;
 }
 
-/// The result from [buildInTempDir]. Note that this object should be disposed after use.
-class DirectoryResult {
-  DirectoryResult(this.directory, this.localBundlePath);
-
-  final Directory directory;
-  final String localBundlePath;
-
-  /// Call this to delete the temporary directory.
-  void dispose() {
-    directory.deleteSync(recursive: true);
-  }
-}
-
 Future<int> build({
   String mainPath: defaultMainPath,
   String manifestPath: defaultManifestPath,
@@ -386,16 +80,6 @@
   bool precompiledSnapshot: false,
   bool includeRobotoFonts: true
 }) async {
-  Object manifest = _loadManifest(manifestPath);
-  if (manifest != null) {
-    int result = await _validateManifest(manifest);
-    if (result != 0)
-      return result;
-  }
-  Map<String, dynamic> manifestDescriptor = manifest;
-
-  String assetBasePath = path.dirname(path.absolute(manifestPath));
-
   File snapshotFile;
 
   if (!precompiledSnapshot) {
@@ -417,9 +101,8 @@
   }
 
   return assemble(
-    manifestDescriptor: manifestDescriptor,
+    manifestPath: manifestPath,
     snapshotFile: snapshotFile,
-    assetBasePath: assetBasePath,
     outputPath: outputPath,
     privateKeyPath: privateKeyPath,
     workingDirPath: workingDirPath,
@@ -428,9 +111,8 @@
 }
 
 Future<int> assemble({
-  Map<String, dynamic> manifestDescriptor: const <String, dynamic>{},
+  String manifestPath,
   File snapshotFile,
-  String assetBasePath: defaultAssetBasePath,
   String outputPath: defaultFlxOutputPath,
   String privateKeyPath: defaultPrivateKeyPath,
   String workingDirPath: defaultWorkingDirPath,
@@ -438,61 +120,22 @@
 }) async {
   printTrace('Building $outputPath');
 
-  final PackageMap packageMap = new PackageMap(path.join(assetBasePath, '.packages'));
-
-  Map<_Asset, List<_Asset>> assetVariants = _parseAssets(
-    packageMap,
-    manifestDescriptor,
-    assetBasePath,
-    excludeDirs: <String>[workingDirPath, path.join(assetBasePath, 'build')]
-  );
-
-  if (assetVariants == null)
-    return 1;
-
-  final bool usesMaterialDesign = manifestDescriptor != null &&
-    manifestDescriptor['uses-material-design'] == true;
+  // Build the asset bundle.
+  AssetBundle assetBundle = new AssetBundle();
+  int result = await assetBundle.build(manifestPath: manifestPath,
+                                       workingDirPath: workingDirPath,
+                                       includeRobotoFonts: includeRobotoFonts);
+  if (result != 0) {
+    return result;
+  }
 
   ZipBuilder zipBuilder = new ZipBuilder();
 
+  // Add all entries from the asset bundle.
+  zipBuilder.entries.addAll(assetBundle.entries);
+
   if (snapshotFile != null)
-    zipBuilder.addEntry(new ZipEntry.fromFile(_kSnapshotKey, snapshotFile));
-
-  for (_Asset asset in assetVariants.keys) {
-    ZipEntry assetEntry = _createAssetEntry(asset);
-    if (assetEntry == null)
-      return 1;
-    zipBuilder.addEntry(assetEntry);
-
-    for (_Asset variant in assetVariants[asset]) {
-      ZipEntry variantEntry = _createAssetEntry(variant);
-      if (variantEntry == null)
-        return 1;
-      zipBuilder.addEntry(variantEntry);
-    }
-  }
-
-  List<_Asset> materialAssets = <_Asset>[];
-  if (usesMaterialDesign) {
-    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);
-  }
-
-  zipBuilder.addEntry(_createAssetManifest(assetVariants));
-
-  ZipEntry fontManifest = _createFontManifest(manifestDescriptor, usesMaterialDesign, includeRobotoFonts);
-  if (fontManifest != null)
-    zipBuilder.addEntry(fontManifest);
-
-  // TODO(ianh): Only do the following line if we've changed packages
-  zipBuilder.addEntry(await _obtainLicenses(packageMap, assetBasePath));
+    zipBuilder.addEntry(new AssetBundleEntry.fromFile(_kSnapshotKey, snapshotFile));
 
   ensureDirectoryExists(outputPath);
 
diff --git a/packages/flutter_tools/lib/src/zip.dart b/packages/flutter_tools/lib/src/zip.dart
index b1edc90..1c7b79b 100644
--- a/packages/flutter_tools/lib/src/zip.dart
+++ b/packages/flutter_tools/lib/src/zip.dart
@@ -2,12 +2,12 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-import 'dart:convert' show UTF8;
 import 'dart:io';
 
 import 'package:archive/archive.dart';
 import 'package:path/path.dart' as path;
 
+import 'asset.dart';
 import 'base/process.dart';
 
 abstract class ZipBuilder {
@@ -21,30 +21,13 @@
 
   ZipBuilder._();
 
-  List<ZipEntry> entries = <ZipEntry>[];
+  List<AssetBundleEntry> entries = <AssetBundleEntry>[];
 
-  void addEntry(ZipEntry entry) => entries.add(entry);
+  void addEntry(AssetBundleEntry entry) => entries.add(entry);
 
   void createZip(File outFile, Directory zipBuildDir);
 }
 
-class ZipEntry {
-  ZipEntry.fromFile(this.archivePath, File file) {
-    this._file = file;
-  }
-
-  ZipEntry.fromString(this.archivePath, String contents) {
-    this._contents = contents;
-  }
-
-  final String archivePath;
-
-  File _file;
-  String _contents;
-
-  bool get isStringEntry => _contents != null;
-}
-
 class _ArchiveZipBuilder extends ZipBuilder {
   _ArchiveZipBuilder() : super._();
 
@@ -52,14 +35,9 @@
   void createZip(File outFile, Directory zipBuildDir) {
     Archive archive = new Archive();
 
-    for (ZipEntry entry in entries) {
-      if (entry.isStringEntry) {
-        List<int> data = UTF8.encode(entry._contents);
-        archive.addFile(new ArchiveFile.noCompress(entry.archivePath, data.length, data));
-      } else {
-        List<int> data = entry._file.readAsBytesSync();
-        archive.addFile(new ArchiveFile(entry.archivePath, data.length, data));
-      }
+    for (AssetBundleEntry entry in entries) {
+      List<int> data = entry.contentsAsBytes();
+      archive.addFile(new ArchiveFile.noCompress(entry.archivePath, data.length, data));
     }
 
     List<int> zipData = new ZipEncoder().encode(archive);
@@ -79,18 +57,11 @@
       zipBuildDir.deleteSync(recursive: true);
     zipBuildDir.createSync(recursive: true);
 
-    for (ZipEntry entry in entries) {
-      if (entry.isStringEntry) {
-        List<int> data = UTF8.encode(entry._contents);
-        File file = new File(path.join(zipBuildDir.path, entry.archivePath));
-        file.parent.createSync(recursive: true);
-        file.writeAsBytesSync(data);
-      } else {
-        List<int> data = entry._file.readAsBytesSync();
-        File file = new File(path.join(zipBuildDir.path, entry.archivePath));
-        file.parent.createSync(recursive: true);
-        file.writeAsBytesSync(data);
-      }
+    for (AssetBundleEntry entry in entries) {
+      List<int> data = entry.contentsAsBytes();
+      File file = new File(path.join(zipBuildDir.path, entry.archivePath));
+      file.parent.createSync(recursive: true);
+      file.writeAsBytesSync(data);
     }
 
     if (_getCompressedNames().isNotEmpty) {
@@ -112,13 +83,13 @@
 
   Iterable<String> _getCompressedNames() {
     return entries
-      .where((ZipEntry entry) => !entry.isStringEntry)
-      .map((ZipEntry entry) => entry.archivePath);
+      .where((AssetBundleEntry entry) => !entry.isStringEntry)
+      .map((AssetBundleEntry entry) => entry.archivePath);
   }
 
   Iterable<String> _getStoredNames() {
     return entries
-      .where((ZipEntry entry) => entry.isStringEntry)
-      .map((ZipEntry entry) => entry.archivePath);
+      .where((AssetBundleEntry entry) => entry.isStringEntry)
+      .map((AssetBundleEntry entry) => entry.archivePath);
   }
 }