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);
}
}