Commit b6644733 authored by John McCutchan's avatar John McCutchan

Support for synchronizing assets onto a DevFS

parent 1fe118fe
// 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;
}
}
......@@ -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<List<int>> contentsAsBytes() async {
if (_isSourceEntry)
return bundleEntry.contentsAsBytes();
return file.readAsBytes();
}
}
......@@ -46,6 +70,7 @@ abstract class DevFSOperations {
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 @@ class ServiceProtocolDevFSOperations implements DevFSOperations {
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;
}
......@@ -91,6 +116,11 @@ class ServiceProtocolDevFSOperations implements DevFSOperations {
}
}
@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,
......@@ -135,7 +165,11 @@ class DevFS {
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 @@ 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<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 @@ 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<dynamic> 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<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;
......
......@@ -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<int> createSnapshot({
String mainPath,
String snapshotPath,
......@@ -54,293 +48,6 @@ Future<int> 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<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 @@ Future<String> 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<int> build({
String mainPath: defaultMainPath,
String manifestPath: defaultManifestPath,
......@@ -386,16 +80,6 @@ Future<int> 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<String, dynamic> manifestDescriptor = manifest;
String assetBasePath = path.dirname(path.absolute(manifestPath));
File snapshotFile;
if (!precompiledSnapshot) {
......@@ -417,9 +101,8 @@ Future<int> build({
}
return assemble(
manifestDescriptor: manifestDescriptor,
manifestPath: manifestPath,
snapshotFile: snapshotFile,
assetBasePath: assetBasePath,
outputPath: outputPath,
privateKeyPath: privateKeyPath,
workingDirPath: workingDirPath,
......@@ -428,9 +111,8 @@ Future<int> build({
}
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 @@ Future<int> assemble({
}) 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;
ZipBuilder zipBuilder = new ZipBuilder();
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);
// 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.addEntry(_createAssetManifest(assetVariants));
ZipBuilder zipBuilder = new ZipBuilder();
ZipEntry fontManifest = _createFontManifest(manifestDescriptor, usesMaterialDesign, includeRobotoFonts);
if (fontManifest != null)
zipBuilder.addEntry(fontManifest);
// Add all entries from the asset bundle.
zipBuilder.entries.addAll(assetBundle.entries);
// TODO(ianh): Only do the following line if we've changed packages
zipBuilder.addEntry(await _obtainLicenses(packageMap, assetBasePath));
if (snapshotFile != null)
zipBuilder.addEntry(new AssetBundleEntry.fromFile(_kSnapshotKey, snapshotFile));
ensureDirectoryExists(outputPath);
......
......@@ -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<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 @@ class _ArchiveZipBuilder extends ZipBuilder {
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);
for (AssetBundleEntry entry in entries) {
List<int> data = entry.contentsAsBytes();
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));
}
}
List<int> 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<int> data = UTF8.encode(entry._contents);
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);
} 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);
}
}
if (_getCompressedNames().isNotEmpty) {
......@@ -112,13 +83,13 @@ class _ZipToolBuilder extends ZipBuilder {
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);
}
}
......@@ -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(<int>[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(<int>[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();
});
......
......@@ -75,6 +75,8 @@ class MockDevFSOperations implements DevFSOperations {
final List<String> messages = new List<String>();
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<dynamic> deleteFile(String fsName, DevFSEntry entry) async {
messages.add('deleteFile $fsName ${entry.devicePath}');
}
@override
Future<dynamic> writeSource(String fsName,
String devicePath,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment