|  | // 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/context.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?'; | 
|  | } | 
|  | printError(message); | 
|  | throw new ProcessExit(2); | 
|  | } | 
|  | } | 
|  |  | 
|  | static void validateSkyEnginePackage() { | 
|  | if (engineRevision == null) { | 
|  | printError("Cannot locate the sky_engine package; did you include 'flutter' in your pubspec.yaml file?"); | 
|  | throw new ProcessExit(2); | 
|  | } | 
|  | if (engineRevision != expectedEngineRevision) { | 
|  | printError("Error: incompatible sky_engine package; please run 'pub get' to get the correct one.\n"); | 
|  | throw new ProcessExit(2); | 
|  | } | 
|  | } | 
|  |  | 
|  | static String _engineRevision; | 
|  |  | 
|  | static String get engineRevision { | 
|  | if (_engineRevision == null) { | 
|  | File revisionFile = new File(path.join(packageRoot, 'sky_engine', 'REVISION')); | 
|  | if (revisionFile.existsSync()) | 
|  | _engineRevision = revisionFile.readAsStringSync(); | 
|  | } | 
|  | return _engineRevision; | 
|  | } | 
|  |  | 
|  | static String _expectedEngineRevision; | 
|  |  | 
|  | static String get expectedEngineRevision { | 
|  | if (_expectedEngineRevision == null) { | 
|  | // TODO(jackson): Parse the .packages file and use the path from there instead | 
|  | File revisionFile = new File(path.join(flutterRoot, 'packages', 'flutter', 'packages', 'sky_engine', 'REVISION')); | 
|  | if (revisionFile.existsSync()) | 
|  | _expectedEngineRevision = revisionFile.readAsStringSync(); | 
|  | } | 
|  | return _expectedEngineRevision; | 
|  | } | 
|  |  | 
|  | 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 { | 
|  | printStatus('Downloading $url.'); | 
|  |  | 
|  | HttpClient httpClient = new HttpClient(); | 
|  | HttpClientRequest request = await httpClient.getUrl(url); | 
|  | HttpClientResponse response = await request.close(); | 
|  | printTrace('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]; | 
|  | } | 
|  | printStatus('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) { | 
|  | printError('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) { | 
|  | validateSkyEnginePackage(); | 
|  | 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()) { | 
|  | printError('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) { | 
|  | printError('Failed to fetch third-party artifact: $url: $e'); | 
|  | throw new ProcessExit(2); | 
|  | } | 
|  | } | 
|  | return cachedFile.path; | 
|  | } | 
|  |  | 
|  | static void clear() { | 
|  | Directory cacheDir = _getBaseCacheDir(); | 
|  | printTrace('Clearing cache directory ${cacheDir.path}'); | 
|  | cacheDir.deleteSync(recursive: true); | 
|  | } | 
|  |  | 
|  | static Future populate() { | 
|  | return Future.wait(knownArtifacts.map((artifact) => getPath(artifact))); | 
|  | } | 
|  | } |