From b6644733c9afd0d9219b932eec6b1688e2323911 Mon Sep 17 00:00:00 2001 From: John McCutchan Date: Fri, 22 Jul 2016 06:59:24 -0700 Subject: [PATCH] Support for synchronizing assets onto a DevFS --- packages/flutter_tools/lib/src/asset.dart | 418 ++++++++++++++++++++ packages/flutter_tools/lib/src/devfs.dart | 88 ++++- packages/flutter_tools/lib/src/flx.dart | 387 +----------------- packages/flutter_tools/lib/src/zip.dart | 59 +-- packages/flutter_tools/test/devfs_test.dart | 27 +- packages/flutter_tools/test/src/mocks.dart | 7 + 6 files changed, 560 insertions(+), 426 deletions(-) create mode 100644 packages/flutter_tools/lib/src/asset.dart diff --git a/packages/flutter_tools/lib/src/asset.dart b/packages/flutter_tools/lib/src/asset.dart new file mode 100644 index 00000000000..d681ca79828 --- /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 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 entries = new Set(); + + static const String defaultManifestPath = 'flutter.yaml'; + static const String defaultWorkingDirPath = 'build/flx'; + static const String _kFontSetMaterial = 'material'; + static const String _kFontSetRoboto = 'roboto'; + + Future 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 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: [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 _readMaterialFontsManifest() { + String fontsPath = path.join(path.absolute(Cache.flutterRoot), + 'packages', 'flutter_tools', 'schema', 'material_fonts.yaml'); + + return loadYaml(new File(fontsPath).readAsStringSync()); +} + +final Map _materialFontsManifest = _readMaterialFontsManifest(); + +List> _getMaterialFonts(String fontSet) { + return _materialFontsManifest[fontSet]; +} + +List<_Asset> _getMaterialAssets(String fontSet) { + List<_Asset> result = <_Asset>[]; + + for (Map family in _getMaterialFonts(fontSet)) { + for (Map 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 _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> packageLicenses = >{}; + 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 rawLicenses = + (await file.readAsString()).split(_licenseSeparator); + for (String rawLicense in rawLicenses) { + String licenseText; + List 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 = [packageName]; + } + packageLicenses.putIfAbsent(rawLicense, () => new Set()) + ..addAll(packageNames); + } + } + } + } + + final List combinedLicensesList = packageLicenses.keys.map( + (String license) { + List 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> json = >{}; + for (_Asset main in assetVariants.keys) { + List variants = []; + 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 manifestDescriptor, + bool usesMaterialDesign, + bool includeRobotoFonts) { + List> fonts = >[]; + 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 manifestDescriptor, + String assetBase, { + List excludeDirs: const [] +}) { + 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 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 family in manifestDescriptor['fonts']) { + List> fonts = family['fonts']; + if (fonts == null) continue; + + for (Map 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 _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 4ac4df27dac..de56314e53f 100644 --- a/packages/flutter_tools/lib/src/devfs.dart +++ b/packages/flutter_tools/lib/src/devfs.dart @@ -9,23 +9,37 @@ import 'dart:io'; 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 @@ class DevFSEntry { } void _stat() { + if (_isSourceEntry) + return; _fileStat = file.statSync(); } + + bool get _isSourceEntry => file == null; + + Future> contentsAsBytes() async { + if (_isSourceEntry) + return bundleEntry.contentsAsBytes(); + return file.readAsBytes(); + } } @@ -46,6 +70,7 @@ abstract class DevFSOperations { Future create(String fsName); Future destroy(String fsName); Future writeFile(String fsName, DevFSEntry entry); + Future deleteFile(String fsName, DevFSEntry entry); Future writeSource(String fsName, String devicePath, String contents); @@ -74,7 +99,7 @@ class ServiceProtocolDevFSOperations implements DevFSOperations { Future writeFile(String fsName, DevFSEntry entry) async { List bytes; try { - bytes = await entry.file.readAsBytes(); + bytes = await entry.contentsAsBytes(); } catch (e) { return e; } @@ -91,6 +116,11 @@ class ServiceProtocolDevFSOperations implements DevFSOperations { } } + @override + Future deleteFile(String fsName, DevFSEntry entry) async { + // TODO(johnmccutchan): Add file deletion to the devFS protocol. + } + @override Future writeSource(String fsName, String devicePath, @@ -135,7 +165,11 @@ class DevFS { return await _operations.destroy(fsName); } - Future update() async { + Future 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 @@ class DevFS { } } } + 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 toRemove = new List(); + _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 @@ class DevFS { 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 @@ class DevFS { entry = new DevFSEntry(devicePath, file); _entries[devicePath] = entry; } + entry._wasSeen = true; bool needsWrite = entry.isModified; if (needsWrite) { Future pendingWrite = _operations.writeFile(fsName, entry); @@ -193,13 +253,29 @@ class DevFS { } } - 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 pendingWrite = _operations.writeFile(fsName, entry); + if (pendingWrite != null) { + _pendingWrites.add(pendingWrite); + } else { + printTrace('DevFS: Failed to sync "$devicePath"'); + } + } + + bool _shouldIgnore(String devicePath) { List ignoredPrefixes = ['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 f9ac4b174bc..7c052be1189 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 defaultWorkingDirPath = 'build/flx'; const String _kSnapshotKey = 'snapshot_blob.bin'; -const String _kFontSetMaterial = 'material'; -const String _kFontSetRoboto = 'roboto'; - Future createSnapshot({ String mainPath, String snapshotPath, @@ -54,293 +48,6 @@ Future createSnapshot({ 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 _readMaterialFontsManifest() { - String fontsPath = path.join(path.absolute(Cache.flutterRoot), - 'packages', 'flutter_tools', 'schema', 'material_fonts.yaml'); - - return loadYaml(new File(fontsPath).readAsStringSync()); -} - -final Map _materialFontsManifest = _readMaterialFontsManifest(); - -List> _getMaterialFonts(String fontSet) { - return _materialFontsManifest[fontSet]; -} - -List<_Asset> _getMaterialAssets(String fontSet) { - List<_Asset> result = <_Asset>[]; - - for (Map family in _getMaterialFonts(fontSet)) { - for (Map 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 manifestDescriptor, - String assetBase, { - List excludeDirs: const [] -}) { - 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 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 family in manifestDescriptor['fonts']) { - List> fonts = family['fonts']; - if (fonts == null) continue; - - for (Map 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 _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> packageLicenses = >{}; - 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 rawLicenses = (await file.readAsString()).split(_licenseSeparator); - for (String rawLicense in rawLicenses) { - String licenseText; - List 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 = [packageName]; - } - packageLicenses.putIfAbsent(rawLicense, () => new Set()) - ..addAll(packageNames); - } - } - } - } - - final List combinedLicensesList = packageLicenses.keys.map( - (String license) { - List 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 _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> json = >{}; - for (_Asset main in assetVariants.keys) { - List variants = []; - 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 manifestDescriptor, - bool usesMaterialDesign, - bool includeRobotoFonts) { - List> fonts = >[]; - 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 @@ Future buildFlx({ 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 build({ String mainPath: defaultMainPath, String manifestPath: defaultManifestPath, @@ -386,16 +80,6 @@ Future build({ 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 manifestDescriptor = manifest; - - String assetBasePath = path.dirname(path.absolute(manifestPath)); - File snapshotFile; if (!precompiledSnapshot) { @@ -417,9 +101,8 @@ Future build({ } return assemble( - manifestDescriptor: manifestDescriptor, + manifestPath: manifestPath, snapshotFile: snapshotFile, - assetBasePath: assetBasePath, outputPath: outputPath, privateKeyPath: privateKeyPath, workingDirPath: workingDirPath, @@ -428,9 +111,8 @@ Future build({ } Future assemble({ - Map manifestDescriptor: const {}, + String manifestPath, File snapshotFile, - String assetBasePath: defaultAssetBasePath, String outputPath: defaultFlxOutputPath, String privateKeyPath: defaultPrivateKeyPath, String workingDirPath: defaultWorkingDirPath, @@ -438,61 +120,22 @@ Future assemble({ }) async { printTrace('Building $outputPath'); - final PackageMap packageMap = new PackageMap(path.join(assetBasePath, '.packages')); - - Map<_Asset, List<_Asset>> assetVariants = _parseAssets( - packageMap, - manifestDescriptor, - assetBasePath, - excludeDirs: [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 b1edc908c08..1c7b79b9f30 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 @@ abstract class ZipBuilder { ZipBuilder._(); - List entries = []; + List entries = []; - 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 @@ class _ArchiveZipBuilder extends ZipBuilder { void createZip(File outFile, Directory zipBuildDir) { Archive archive = new Archive(); - for (ZipEntry entry in entries) { - if (entry.isStringEntry) { - List data = UTF8.encode(entry._contents); - archive.addFile(new ArchiveFile.noCompress(entry.archivePath, data.length, data)); - } else { - List data = entry._file.readAsBytesSync(); - archive.addFile(new ArchiveFile(entry.archivePath, data.length, data)); - } + for (AssetBundleEntry entry in entries) { + List data = entry.contentsAsBytes(); + archive.addFile(new ArchiveFile.noCompress(entry.archivePath, data.length, data)); } List zipData = new ZipEncoder().encode(archive); @@ -79,18 +57,11 @@ class _ZipToolBuilder extends ZipBuilder { zipBuildDir.deleteSync(recursive: true); zipBuildDir.createSync(recursive: true); - for (ZipEntry entry in entries) { - if (entry.isStringEntry) { - List 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 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 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 @@ class _ZipToolBuilder extends ZipBuilder { Iterable _getCompressedNames() { return entries - .where((ZipEntry entry) => !entry.isStringEntry) - .map((ZipEntry entry) => entry.archivePath); + .where((AssetBundleEntry entry) => !entry.isStringEntry) + .map((AssetBundleEntry entry) => entry.archivePath); } Iterable _getStoredNames() { return entries - .where((ZipEntry entry) => entry.isStringEntry) - .map((ZipEntry entry) => entry.archivePath); + .where((AssetBundleEntry entry) => entry.isStringEntry) + .map((AssetBundleEntry entry) => entry.archivePath); } } diff --git a/packages/flutter_tools/test/devfs_test.dart b/packages/flutter_tools/test/devfs_test.dart index 3f8f18b970e..67a8a1ced3b 100644 --- a/packages/flutter_tools/test/devfs_test.dart +++ b/packages/flutter_tools/test/devfs_test.dart @@ -4,6 +4,7 @@ import 'dart:io'; +import 'package:flutter_tools/src/asset.dart'; import 'package:flutter_tools/src/devfs.dart'; import 'package:path/path.dart' as path; import 'package:test/test.dart'; @@ -18,6 +19,8 @@ void main() { String basePath; MockDevFSOperations devFSOperations = new MockDevFSOperations(); DevFS devFS; + AssetBundle assetBundle = new AssetBundle(); + assetBundle.entries.add(new AssetBundleEntry.fromString('a.txt', '')); group('devfs', () { testUsingContext('create local file system', () async { tempDir = Directory.systemTemp.createTempSync(); @@ -38,8 +41,6 @@ void main() { testUsingContext('modify existing file on local file system', () async { File file = new File(path.join(basePath, filePath)); file.writeAsBytesSync([1, 2, 3, 4, 5, 6]); - }); - testUsingContext('update dev file system', () async { await devFS.update(); expect(devFSOperations.contains('writeFile test bar/foo.txt'), isTrue); }); @@ -47,11 +48,29 @@ void main() { File file = new File(path.join(basePath, filePath2)); await file.parent.create(recursive: true); file.writeAsBytesSync([1, 2, 3, 4, 5, 6, 7]); - }); - testUsingContext('update dev file system', () async { await devFS.update(); expect(devFSOperations.contains('writeFile test foo/bar.txt'), isTrue); }); + testUsingContext('delete a file from the local file system', () async { + File file = new File(path.join(basePath, filePath)); + await file.delete(); + await devFS.update(); + expect(devFSOperations.contains('deleteFile test bar/foo.txt'), isTrue); + }); + testUsingContext('add file in an asset bundle', () async { + await devFS.update(assetBundle); + expect(devFSOperations.contains('writeFile test build/flx/a.txt'), isTrue); + }); + testUsingContext('add a file to the asset bundle', () async { + assetBundle.entries.add(new AssetBundleEntry.fromString('b.txt', '')); + await devFS.update(assetBundle); + expect(devFSOperations.contains('writeFile test build/flx/b.txt'), isTrue); + }); + testUsingContext('delete a file from the asset bundle', () async { + assetBundle.entries.clear(); + await devFS.update(assetBundle); + expect(devFSOperations.contains('deleteFile test build/flx/b.txt'), isTrue); + }); testUsingContext('delete dev file system', () async { await devFS.destroy(); }); diff --git a/packages/flutter_tools/test/src/mocks.dart b/packages/flutter_tools/test/src/mocks.dart index 49706e70a8b..a384ea17e26 100644 --- a/packages/flutter_tools/test/src/mocks.dart +++ b/packages/flutter_tools/test/src/mocks.dart @@ -75,6 +75,8 @@ class MockDevFSOperations implements DevFSOperations { final List messages = new List(); bool contains(String match) { + print('Checking for `$match` in:'); + print(messages); bool result = messages.contains(match); messages.clear(); return result; @@ -96,6 +98,11 @@ class MockDevFSOperations implements DevFSOperations { messages.add('writeFile $fsName ${entry.devicePath}'); } + @override + Future deleteFile(String fsName, DevFSEntry entry) async { + messages.add('deleteFile $fsName ${entry.devicePath}'); + } + @override Future writeSource(String fsName, String devicePath,