flutter/packages/flutter_tools/lib/src/artifacts.dart
Matt Perry 6610b7ea04 Support local paths to third-party jars in flutter apk.
Also improve the error message a bit if a download fails.
2016-01-25 15:05:06 -05:00

351 lines
11 KiB
Dart

// Copyright 2015 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:io';
import 'package:archive/archive.dart';
import 'package:path/path.dart' as path;
import 'base/logging.dart';
import 'base/os.dart';
import 'base/process.dart';
import 'build_configuration.dart';
String _getNameForHostPlatform(HostPlatform platform) {
switch (platform) {
case HostPlatform.linux:
return 'linux-x64';
case HostPlatform.mac:
return 'darwin-x64';
}
}
String _getNameForTargetPlatform(TargetPlatform platform) {
switch (platform) {
case TargetPlatform.android:
return 'android-arm';
case TargetPlatform.iOS:
return 'ios-arm';
case TargetPlatform.iOSSimulator:
return 'ios-x64';
case TargetPlatform.mac:
return 'darwin-x64';
case TargetPlatform.linux:
return 'linux-x64';
}
}
// Keep in sync with https://github.com/flutter/engine/blob/master/sky/tools/release_engine.py
// and https://github.com/flutter/buildbot/blob/master/travis/build.sh
String _getCloudStorageBaseUrl({String platform, String revision}) {
return 'https://storage.googleapis.com/mojo_infra/flutter/$revision/$platform/';
}
enum ArtifactType {
snapshot,
shell,
mojo,
androidClassesJar,
androidIcuData,
androidKeystore,
androidLibSkyShell,
}
class Artifact {
const Artifact._({
this.name,
this.fileName,
this.type,
this.hostPlatform,
this.targetPlatform
});
final String name;
final String fileName;
final ArtifactType type;
final HostPlatform hostPlatform;
final TargetPlatform targetPlatform;
String get platform {
if (targetPlatform != null)
return _getNameForTargetPlatform(targetPlatform);
if (hostPlatform != null)
return _getNameForHostPlatform(hostPlatform);
assert(false);
return null;
}
// Whether the artifact needs to be marked as executable on disk.
bool get executable {
return type == ArtifactType.snapshot ||
(type == ArtifactType.shell && targetPlatform == TargetPlatform.linux);
}
}
class ArtifactStore {
static const List<Artifact> knownArtifacts = const <Artifact>[
const Artifact._(
name: 'Sky Shell',
fileName: 'SkyShell.apk',
type: ArtifactType.shell,
targetPlatform: TargetPlatform.android
),
const Artifact._(
name: 'Sky Shell',
fileName: 'sky_shell',
type: ArtifactType.shell,
targetPlatform: TargetPlatform.linux
),
const Artifact._(
name: 'Sky Snapshot',
fileName: 'sky_snapshot',
type: ArtifactType.snapshot,
hostPlatform: HostPlatform.linux
),
const Artifact._(
name: 'Sky Snapshot',
fileName: 'sky_snapshot',
type: ArtifactType.snapshot,
hostPlatform: HostPlatform.mac
),
const Artifact._(
name: 'Flutter for Mojo',
fileName: 'flutter.mojo',
type: ArtifactType.mojo,
targetPlatform: TargetPlatform.android
),
const Artifact._(
name: 'Flutter for Mojo',
fileName: 'flutter.mojo',
type: ArtifactType.mojo,
targetPlatform: TargetPlatform.linux
),
const Artifact._(
name: 'Compiled Java code',
fileName: 'classes.dex.jar',
type: ArtifactType.androidClassesJar,
targetPlatform: TargetPlatform.android
),
const Artifact._(
name: 'ICU data table',
fileName: 'icudtl.dat',
type: ArtifactType.androidIcuData,
targetPlatform: TargetPlatform.android
),
const Artifact._(
name: 'Key Store',
fileName: 'chromium-debug.keystore',
type: ArtifactType.androidKeystore,
targetPlatform: TargetPlatform.android
),
const Artifact._(
name: 'Compiled C++ code',
fileName: 'libsky_shell.so',
type: ArtifactType.androidLibSkyShell,
targetPlatform: TargetPlatform.android
),
];
static Artifact getArtifact({
ArtifactType type,
HostPlatform hostPlatform,
TargetPlatform targetPlatform
}) {
for (Artifact artifact in ArtifactStore.knownArtifacts) {
if (type != null &&
type != artifact.type)
continue;
if (hostPlatform != null &&
artifact.hostPlatform != null &&
hostPlatform != artifact.hostPlatform)
continue;
if (targetPlatform != null &&
artifact.targetPlatform != null &&
targetPlatform != artifact.targetPlatform)
continue;
return artifact;
}
return null;
}
// These values are initialized by FlutterCommandRunner on startup.
static String flutterRoot;
static String packageRoot = 'packages';
static bool get isPackageRootValid {
return FileSystemEntity.isDirectorySync(packageRoot);
}
static void ensurePackageRootIsValid() {
if (!isPackageRootValid) {
String message = '$packageRoot is not a valid directory.';
if (packageRoot == 'packages') {
if (FileSystemEntity.isFileSync('pubspec.yaml'))
message += '\nDid you run `pub get` in this directory?';
else
message += '\nDid you run this command from the same directory as your pubspec.yaml file?';
}
stderr.writeln(message);
throw new ProcessExit(2);
}
}
static void ensureHasSkyEnginePackage() {
Directory skyEnginePackage = new Directory(path.join(packageRoot, 'sky_engine'));
if (!skyEnginePackage.existsSync()) {
stderr.writeln("Cannot locate the sky_engine package; did you include 'flutter' in your pubspec.yaml file?");
throw new ProcessExit(2);
}
}
static String _engineRevision;
static String get engineRevision {
if (_engineRevision == null) {
ensurePackageRootIsValid();
File revisionFile = new File(path.join(packageRoot, 'sky_engine', 'REVISION'));
if (revisionFile.existsSync())
_engineRevision = revisionFile.readAsStringSync();
}
return _engineRevision;
}
static String getCloudStorageBaseUrl(String platform) {
return _getCloudStorageBaseUrl(
platform: platform,
revision: engineRevision
);
}
/// Download a file from the given URL and return the bytes.
static Future<List<int>> _downloadFile(Uri url) async {
logging.info('Downloading $url.');
HttpClient httpClient = new HttpClient();
HttpClientRequest request = await httpClient.getUrl(url);
HttpClientResponse response = await request.close();
logging.fine('Received response statusCode=${response.statusCode}');
if (response.statusCode != 200)
throw new Exception(response.reasonPhrase);
BytesBuilder responseBody = new BytesBuilder(copy: false);
await for (List<int> chunk in response) {
responseBody.add(chunk);
}
return responseBody.takeBytes();
}
/// Download a file from the given url and write it to the cache.
static Future _downloadFileToCache(Uri url, File cachedFile) async {
if (!cachedFile.parent.existsSync())
cachedFile.parent.createSync(recursive: true);
List<int> fileBytes = await _downloadFile(url);
cachedFile.writeAsBytesSync(fileBytes, flush: true);
}
/// Download the artifacts.zip archive for the given platform from GCS
/// and extract it to the local cache.
static Future _doDownloadArtifactsFromZip(String platform) async {
String url = getCloudStorageBaseUrl(platform) + 'artifacts.zip';
List<int> zipBytes = await _downloadFile(Uri.parse(url));
Archive archive = new ZipDecoder().decodeBytes(zipBytes);
Directory cacheDir = _getCacheDirForPlatform(platform);
for (ArchiveFile archiveFile in archive) {
File cacheFile = new File(path.join(cacheDir.path, archiveFile.name));
cacheFile.writeAsBytesSync(archiveFile.content, flush: true);
}
for (Artifact artifact in knownArtifacts) {
if (artifact.platform == platform && artifact.executable) {
ProcessResult result = os.makeExecutable(
new File(path.join(cacheDir.path, artifact.fileName)));
if (result.exitCode != 0)
throw new Exception(result.stderr);
}
}
}
/// A wrapper ensuring that a platform's ZIP is not downloaded multiple times
/// concurrently.
static Future _downloadArtifactsFromZip(String platform) {
if (_pendingZipDownloads.containsKey(platform)) {
return _pendingZipDownloads[platform];
}
print('Downloading $platform artifacts from the cloud, one moment please...');
Future future = _doDownloadArtifactsFromZip(platform);
_pendingZipDownloads[platform] = future;
return future.then((_) => _pendingZipDownloads.remove(platform));
}
static final Map<String, Future> _pendingZipDownloads = new Map<String, Future>();
static Directory _getBaseCacheDir() {
if (flutterRoot == null) {
logging.severe('FLUTTER_ROOT not specified. Cannot find artifact cache.');
throw new ProcessExit(2);
}
Directory cacheDir = new Directory(path.join(flutterRoot, 'bin', 'cache', 'artifacts'));
if (!cacheDir.existsSync())
cacheDir.createSync(recursive: true);
return cacheDir;
}
static Directory _getCacheDirForPlatform(String platform) {
ensureHasSkyEnginePackage();
Directory baseDir = _getBaseCacheDir();
// TODO(jamesr): Add support for more configurations.
String config = 'Release';
Directory artifactSpecificDir = new Directory(path.join(
baseDir.path, 'sky_engine', engineRevision, config, platform));
if (!artifactSpecificDir.existsSync())
artifactSpecificDir.createSync(recursive: true);
return artifactSpecificDir;
}
static Future<String> getPath(Artifact artifact) async {
Directory cacheDir = _getCacheDirForPlatform(artifact.platform);
File cachedFile = new File(path.join(cacheDir.path, artifact.fileName));
if (!cachedFile.existsSync()) {
await _downloadArtifactsFromZip(artifact.platform);
if (!cachedFile.existsSync()) {
logging.severe('File not found in the platform artifacts: ${cachedFile.path}');
throw new ProcessExit(2);
}
}
return cachedFile.path;
}
static Future<String> getThirdPartyFile(String urlStr, String cacheSubdir) async {
Uri url = Uri.parse(urlStr);
Directory baseDir = _getBaseCacheDir();
Directory cacheDir = new Directory(path.join(
baseDir.path, 'third_party', cacheSubdir));
File cachedFile = new File(
path.join(cacheDir.path, url.pathSegments[url.pathSegments.length-1]));
if (!cachedFile.existsSync()) {
try {
await _downloadFileToCache(url, cachedFile);
} catch (e) {
logging.severe('Failed to fetch third-party artifact: $url: $e');
throw new ProcessExit(2);
}
}
return cachedFile.path;
}
static void clear() {
Directory cacheDir = _getBaseCacheDir();
logging.fine('Clearing cache directory ${cacheDir.path}');
cacheDir.deleteSync(recursive: true);
}
static Future populate() {
return Future.wait(knownArtifacts.map((artifact) => getPath(artifact)));
}
}