diff --git a/packages/flutter_tools/lib/flutter_tools.dart b/packages/flutter_tools/lib/flutter_tools.dart index e1c513a6e0..dfd7a94cfd 100644 --- a/packages/flutter_tools/lib/flutter_tools.dart +++ b/packages/flutter_tools/lib/flutter_tools.dart @@ -3,8 +3,7 @@ // found in the LICENSE file. import 'dart:async'; - -import 'package:archive/archive.dart'; +import 'dart:io'; import 'src/flx.dart' as flx; @@ -12,7 +11,7 @@ import 'src/flx.dart' as flx; /// pre-compiled snapshot. Future assembleFlx({ Map manifestDescriptor: const {}, - ArchiveFile snapshotFile: null, + File snapshotFile: null, String assetBasePath: flx.defaultAssetBasePath, String materialAssetBasePath: flx.defaultMaterialAssetBasePath, String outputPath: flx.defaultFlxOutputPath, diff --git a/packages/flutter_tools/lib/src/android/android_device.dart b/packages/flutter_tools/lib/src/android/android_device.dart index aa09f7a9a8..2252b6e589 100644 --- a/packages/flutter_tools/lib/src/android/android_device.dart +++ b/packages/flutter_tools/lib/src/android/android_device.dart @@ -5,7 +5,6 @@ import 'dart:async'; import 'dart:io'; -import 'package:crypto/crypto.dart'; import 'package:path/path.dart' as path; import '../android/android_sdk.dart'; @@ -136,26 +135,16 @@ class AndroidDevice extends Device { } String _getSourceSha1(ApplicationPackage app) { - var sha1 = new SHA1(); - var file = new File(app.localPath); - sha1.add(file.readAsBytesSync()); - return CryptoUtils.bytesToHex(sha1.close()); + File shaFile = new File('${app.localPath}.sha1'); + return shaFile.existsSync() ? shaFile.readAsStringSync() : ''; } String get name => modelID; @override bool isAppInstalled(ApplicationPackage app) { - if (runCheckedSync(adbCommandForDevice(['shell', 'pm', 'path', app.id])) == '') { - printTrace('TODO(iansf): move this log to the caller. ${app.name} is not on the device. Installing now...'); - return false; - } - if (_getDeviceApkSha1(app) != _getSourceSha1(app)) { - printTrace( - 'TODO(iansf): move this log to the caller. ${app.name} is out of date. Installing now...'); - return false; - } - return true; + // Just check for the existence of the application SHA. + return _getDeviceApkSha1(app) == _getSourceSha1(app); } @override @@ -179,7 +168,7 @@ class AndroidDevice extends Device { if (port == 0) { // Auto-bind to a port. Set up forwarding for that port. Emit a stdout - // message similar to the command-line VM, so that tools can parse the output. + // message similar to the command-line VM so that tools can parse the output. // "Observatory listening on http://127.0.0.1:52111" port = await findAvailablePort(); } @@ -258,30 +247,26 @@ class AndroidDevice extends Device { if (!_checkForSupportedAdbVersion() || !_checkForSupportedAndroidVersion()) return false; - flx.DirectoryResult buildResult = await flx.buildInTempDir( + String localBundlePath = await flx.buildFlx( toolchain, mainPath: mainPath ); printTrace('Starting bundle for $this.'); - try { - if (await startBundle( - package, - buildResult.localBundlePath, - checked: checked, - traceStartup: platformArgs['trace-startup'], - route: route, - clearLogs: clearLogs, - startPaused: startPaused, - debugPort: debugPort - )) { - return true; - } else { - return false; - } - } finally { - buildResult.dispose(); + if (await startBundle( + package, + localBundlePath, + checked: checked, + traceStartup: platformArgs['trace-startup'], + route: route, + clearLogs: clearLogs, + startPaused: startPaused, + debugPort: debugPort + )) { + return true; + } else { + return false; } } diff --git a/packages/flutter_tools/lib/src/base/process.dart b/packages/flutter_tools/lib/src/base/process.dart index 1009e96c98..5a5f419910 100644 --- a/packages/flutter_tools/lib/src/base/process.dart +++ b/packages/flutter_tools/lib/src/base/process.dart @@ -65,8 +65,12 @@ Future runDetached(List cmd) { /// Run cmd and return stdout. /// Throws an error if cmd exits with a non-zero value. -String runCheckedSync(List cmd, { String workingDirectory }) { - return _runWithLoggingSync(cmd, workingDirectory: workingDirectory, checked: true, noisyErrors: true); +String runCheckedSync(List cmd, { + String workingDirectory, bool truncateCommand: false +}) { + return _runWithLoggingSync( + cmd, workingDirectory: workingDirectory, checked: true, noisyErrors: true, truncateCommand: truncateCommand + ); } /// Run cmd and return stdout. @@ -91,9 +95,13 @@ bool exitsHappy(List cli) { String _runWithLoggingSync(List cmd, { bool checked: false, bool noisyErrors: false, - String workingDirectory + String workingDirectory, + bool truncateCommand: false }) { - printTrace(cmd.join(' ')); + String cmdText = cmd.join(' '); + if (truncateCommand && cmdText.length > 160) + cmdText = cmdText.substring(0, 160) + '…'; + printTrace(cmdText); ProcessResult results = Process.runSync(cmd[0], cmd.getRange(1, cmd.length).toList(), workingDirectory: workingDirectory); if (results.exitCode != 0) { diff --git a/packages/flutter_tools/lib/src/base/utils.dart b/packages/flutter_tools/lib/src/base/utils.dart index b271fc18e5..e7f5412e7c 100644 --- a/packages/flutter_tools/lib/src/base/utils.dart +++ b/packages/flutter_tools/lib/src/base/utils.dart @@ -3,6 +3,15 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:io'; + +import 'package:crypto/crypto.dart'; + +String calculateSha(File file) { + SHA1 sha1 = new SHA1(); + sha1.add(file.readAsBytesSync()); + return CryptoUtils.bytesToHex(sha1.close()); +} /// A class to maintain a list of items, fire events when items are added or /// removed, and calculate a diff of changes when a new list of items is diff --git a/packages/flutter_tools/lib/src/commands/apk.dart b/packages/flutter_tools/lib/src/commands/apk.dart index fb3ee01a28..ac190de6ba 100644 --- a/packages/flutter_tools/lib/src/commands/apk.dart +++ b/packages/flutter_tools/lib/src/commands/apk.dart @@ -13,6 +13,7 @@ import '../artifacts.dart'; import '../base/file_system.dart' show ensureDirectoryExists; import '../base/os.dart'; import '../base/process.dart'; +import '../base/utils.dart'; import '../build_configuration.dart'; import '../device.dart'; import '../flx.dart' as flx; @@ -296,6 +297,9 @@ int _buildApk( ensureDirectoryExists(finalApk.path); builder.align(unalignedApk, finalApk); + File apkShaFile = new File('$outputFile.sha1'); + apkShaFile.writeAsStringSync(calculateSha(finalApk)); + printStatus('Generated APK to ${finalApk.path}.'); return 0; @@ -313,7 +317,7 @@ int _signApk( String keyPassword; if (keystoreInfo == null) { - printError('Signing the APK using the debug keystore.'); + printStatus('Warning: signing the APK using the debug keystore.'); keystore = components.debugKeystore; keystorePassword = _kDebugKeystorePassword; keyAlias = _kDebugKeystoreKeyAlias; @@ -345,13 +349,14 @@ bool _needsRebuild(String apkPath, String manifest) { Iterable dependenciesStat = [ manifest, _kFlutterManifestPath, - _kPackagesStatusPath + _kPackagesStatusPath, + '$apkPath.sha1' ].map((String path) => FileStat.statSync(path)); if (apkStat.type == FileSystemEntityType.NOT_FOUND) return true; for (FileStat dep in dependenciesStat) { - if (dep.modified.isAfter(apkStat.modified)) + if (dep.modified == null || dep.modified.isAfter(apkStat.modified)) return true; } return false; @@ -381,7 +386,7 @@ Future buildAndroid({ } if (!force && !_needsRebuild(outputFile, manifest)) { - printTrace('APK up to date. Skipping build step.'); + printTrace('APK up to date; skipping build step.'); return 0; } @@ -408,13 +413,8 @@ Future buildAndroid({ String mainPath = findMainDartFile(target); // Build the FLX. - flx.DirectoryResult buildResult = await flx.buildInTempDir(toolchain, mainPath: mainPath); - - try { - return _buildApk(components, buildResult.localBundlePath, keystore, outputFile); - } finally { - buildResult.dispose(); - } + String localBundlePath = await flx.buildFlx(toolchain, mainPath: mainPath); + return _buildApk(components, localBundlePath, keystore, outputFile); } } diff --git a/packages/flutter_tools/lib/src/flx.dart b/packages/flutter_tools/lib/src/flx.dart index 43b52dc87b..8e72b438e1 100644 --- a/packages/flutter_tools/lib/src/flx.dart +++ b/packages/flutter_tools/lib/src/flx.dart @@ -5,9 +5,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; -import 'dart:typed_data'; -import 'package:archive/archive.dart'; import 'package:flx/bundle.dart'; import 'package:flx/signing.dart'; import 'package:path/path.dart' as path; @@ -16,6 +14,7 @@ import 'package:yaml/yaml.dart'; import 'base/file_system.dart' show ensureDirectoryExists; import 'globals.dart'; import 'toolchain.dart'; +import 'zip.dart'; const String defaultMainPath = 'lib/main.dart'; const String defaultAssetBasePath = '.'; @@ -140,20 +139,17 @@ dynamic _loadManifest(String manifestPath) { return loadYaml(manifestDescriptor); } -bool _addAssetFile(Archive archive, _Asset asset) { +ZipEntry _createAssetEntry(_Asset asset) { String source = asset.source ?? asset.key; File file = new File('${asset.base}/$source'); if (!file.existsSync()) { printError('Cannot find asset "$source" in directory "${path.absolute(asset.base)}".'); - return false; + return null; } - List content = file.readAsBytesSync(); - archive.addFile(new ArchiveFile.noCompress(asset.key, content.length, content)); - return true; + return new ZipEntry.fromFile(asset.key, file); } -ArchiveFile _createAssetManifest(Map<_Asset, List<_Asset>> assets) { - String key = 'AssetManifest.json'; +ZipEntry _createAssetManifest(Map<_Asset, List<_Asset>> assets) { Map> json = >{}; for (_Asset main in assets.keys) { List variants = []; @@ -161,34 +157,25 @@ ArchiveFile _createAssetManifest(Map<_Asset, List<_Asset>> assets) { variants.add(variant.key); json[main.key] = variants; } - List content = UTF8.encode(JSON.encode(json)); - return new ArchiveFile.noCompress(key, content.length, content); + return new ZipEntry.fromString('AssetManifest.json', JSON.encode(json)); } -ArchiveFile _createFontManifest(Map manifestDescriptor) { +ZipEntry _createFontManifest(Map manifestDescriptor) { if (manifestDescriptor != null && manifestDescriptor.containsKey('fonts')) { - List content = UTF8.encode(JSON.encode(manifestDescriptor['fonts'])); - return new ArchiveFile.noCompress('FontManifest.json', content.length, content); + return new ZipEntry.fromString('FontManifest.json', JSON.encode(manifestDescriptor['fonts'])); } else { return null; } } -ArchiveFile _createSnapshotFile(String snapshotPath) { - File file = new File(snapshotPath); - List content = file.readAsBytesSync(); - return new ArchiveFile(_kSnapshotKey, content.length, content); -} - -/// Build the flx in a temp dir and return `localBundlePath` on success. -Future buildInTempDir( +/// Build the flx in the build/ directory and return `localBundlePath` on success. +Future buildFlx( Toolchain toolchain, { String mainPath: defaultMainPath }) async { int result; - Directory tempDir = await Directory.systemTemp.createTemp('flutter_tools'); - String localBundlePath = path.join(tempDir.path, 'app.flx'); - String localSnapshotPath = path.join(tempDir.path, 'snapshot_blob.bin'); + String localBundlePath = path.join('build', 'app.flx'); + String localSnapshotPath = path.join('build', 'snapshot_blob.bin'); result = await build( toolchain, snapshotPath: localSnapshotPath, @@ -196,7 +183,7 @@ Future buildInTempDir( mainPath: mainPath ); if (result == 0) - return new DirectoryResult(tempDir, localBundlePath); + return localBundlePath; else throw result; } @@ -227,7 +214,8 @@ Future build( Map manifestDescriptor = _loadManifest(manifestPath); String assetBasePath = path.dirname(path.absolute(manifestPath)); - ArchiveFile snapshotFile = null; + File snapshotFile; + if (!precompiledSnapshot) { ensureDirectoryExists(snapshotPath); @@ -239,7 +227,7 @@ Future build( return result; } - snapshotFile = _createSnapshotFile(snapshotPath); + snapshotFile = new File(snapshotPath); } return assemble( @@ -254,7 +242,7 @@ Future build( Future assemble({ Map manifestDescriptor: const {}, - ArchiveFile snapshotFile, + File snapshotFile, String assetBasePath: defaultAssetBasePath, String materialAssetBasePath: defaultMaterialAssetBasePath, String outputPath: defaultFlxOutputPath, @@ -265,25 +253,32 @@ Future assemble({ Map<_Asset, List<_Asset>> assets = _parseAssets(manifestDescriptor, assetBasePath); assets.addAll(_parseMaterialAssets(manifestDescriptor, materialAssetBasePath)); - Archive archive = new Archive(); + ZipBuilder zipBuilder = new ZipBuilder(); if (snapshotFile != null) - archive.addFile(snapshotFile); + zipBuilder.addEntry(new ZipEntry.fromFile(_kSnapshotKey, snapshotFile)); for (_Asset asset in assets.keys) { - if (!_addAssetFile(archive, asset)) + ZipEntry assetEntry = _createAssetEntry(asset); + if (assetEntry == null) return 1; + else + zipBuilder.addEntry(assetEntry); + for (_Asset variant in assets[asset]) { - if (!_addAssetFile(archive, variant)) + ZipEntry variantEntry = _createAssetEntry(variant); + if (variantEntry == null) return 1; + else + zipBuilder.addEntry(variantEntry); } } - archive.addFile(_createAssetManifest(assets)); + zipBuilder.addEntry(_createAssetManifest(assets)); - ArchiveFile fontManifest = _createFontManifest(manifestDescriptor); + ZipEntry fontManifest = _createFontManifest(manifestDescriptor); if (fontManifest != null) - archive.addFile(fontManifest); + zipBuilder.addEntry(fontManifest); AsymmetricKeyPair keyPair = keyPairFromPrivateKeyFileSync(privateKeyPath); printTrace('KeyPair from $privateKeyPath: $keyPair.'); @@ -293,8 +288,10 @@ Future assemble({ CipherParameters.get().seedRandom(); } - printTrace('Encoding zip file.'); - Uint8List zipBytes = new Uint8List.fromList(new ZipEncoder().encode(archive)); + File zipFile = new File(outputPath.substring(0, outputPath.length - 4) + '.zip'); + printTrace('Encoding zip file to ${zipFile.path}'); + zipBuilder.createZip(zipFile, new Directory('build/flx')); + List zipBytes = zipFile.readAsBytesSync(); ensureDirectoryExists(outputPath); @@ -307,7 +304,7 @@ Future assemble({ ); bundle.writeSync(); - printTrace('Built and signed flx at $outputPath.'); + printTrace('Built $outputPath.'); return 0; } diff --git a/packages/flutter_tools/lib/src/zip.dart b/packages/flutter_tools/lib/src/zip.dart new file mode 100644 index 0000000000..1eb874bfe4 --- /dev/null +++ b/packages/flutter_tools/lib/src/zip.dart @@ -0,0 +1,117 @@ +// 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:io'; +import 'dart:convert' show UTF8; + +import 'package:archive/archive.dart'; +import 'package:path/path.dart' as path; + +import 'base/process.dart'; + +abstract class ZipBuilder { + factory ZipBuilder() { + if (exitsHappy(['which', 'zip'])) { + return new _ZipToolBuilder(); + } else { + return new _ArchiveZipBuilder(); + } + } + + ZipBuilder._(); + + List entries = []; + + void addEntry(ZipEntry 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._(); + + 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)); + } + } + + List zipData = new ZipEncoder().encode(archive); + outFile.writeAsBytesSync(zipData); + } +} + +class _ZipToolBuilder extends ZipBuilder { + _ZipToolBuilder() : super._(); + + void createZip(File outFile, Directory zipBuildDir) { + if (outFile.existsSync()) + outFile.deleteSync(); + + if (zipBuildDir.existsSync()) + 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); + } + } + + runCheckedSync( + ['zip', '-q', outFile.absolute.path]..addAll(_getCompressedNames()), + workingDirectory: zipBuildDir.path, + truncateCommand: true + ); + runCheckedSync( + ['zip', '-q', '-0', outFile.absolute.path]..addAll(_getStoredNames()), + workingDirectory: zipBuildDir.path, + truncateCommand: true + ); + } + + Iterable _getCompressedNames() { + return entries + .where((ZipEntry entry) => !entry.isStringEntry) + .map((ZipEntry entry) => entry.archivePath); + } + + Iterable _getStoredNames() { + return entries + .where((ZipEntry entry) => entry.isStringEntry) + .map((ZipEntry entry) => entry.archivePath); + } +} diff --git a/packages/flx/lib/bundle.dart b/packages/flx/lib/bundle.dart index 0b7fe3725c..613cf21e68 100644 --- a/packages/flx/lib/bundle.dart +++ b/packages/flx/lib/bundle.dart @@ -71,7 +71,7 @@ class Bundle { Bundle.fromContent({ this.path, this.manifest, - contentBytes, + List contentBytes, AsymmetricKeyPair keyPair }) : _contentBytes = contentBytes { assert(path != null);