mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
commit
c8f66a1f6c
52
packages/flutter_tools/README.md
Normal file
52
packages/flutter_tools/README.md
Normal file
@ -0,0 +1,52 @@
|
||||
# tools
|
||||
|
||||
[](https://travis-ci.org/flutter/tools)
|
||||
[](https://ci.appveyor.com/project/devoncarew/tools/branch/master)
|
||||
[](https://pub.dartlang.org/packages/sky_tools)
|
||||
|
||||
Tools for building Flutter applications.
|
||||
|
||||
## Installing
|
||||
|
||||
To install, run:
|
||||
|
||||
pub global activate sky_tools
|
||||
|
||||
or, depend on this package in your pubspec:
|
||||
|
||||
```yaml
|
||||
dev_dependencies:
|
||||
sky_tools: any
|
||||
```
|
||||
|
||||
## Running sky_tools
|
||||
|
||||
Run `sky_tools` (or `pub global run sky_tools`) to see a list of available
|
||||
commands:
|
||||
|
||||
- `init` to create a new project
|
||||
|
||||
Then, run a `sky_tools` command:
|
||||
|
||||
sky_tools init --out my_sky_project
|
||||
|
||||
## Running sky_tools:sky_server
|
||||
|
||||
To serve the current directory using `sky_server`:
|
||||
|
||||
pub run sky_tools:sky_server [-v] PORT
|
||||
|
||||
## Running sky_tools:build_sky_apk
|
||||
|
||||
```
|
||||
usage: pub run sky_tools:build_sky_apk <options>
|
||||
|
||||
-h, --help
|
||||
--android-sdk
|
||||
--skyx
|
||||
```
|
||||
|
||||
## Filing Issues
|
||||
|
||||
Please file reports on the
|
||||
[GitHub Issue Tracker](https://github.com/flutter/tools/issues).
|
126
packages/flutter_tools/bin/build_sky_apk.dart
Normal file
126
packages/flutter_tools/bin/build_sky_apk.dart
Normal file
@ -0,0 +1,126 @@
|
||||
// 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:io';
|
||||
|
||||
import 'package:args/args.dart';
|
||||
|
||||
const String kBuildToolsVersion = '22.0.1';
|
||||
const String kAndroidPlatformVersion = '22';
|
||||
|
||||
const String kKeystoreKeyName = "chromiumdebugkey";
|
||||
const String kKeystorePassword = "chromium";
|
||||
|
||||
class AssetBuilder {
|
||||
final Directory outDir;
|
||||
|
||||
Directory _assetDir;
|
||||
|
||||
AssetBuilder(this.outDir) {
|
||||
_assetDir = new Directory('${outDir.path}/assets');
|
||||
_assetDir.createSync(recursive: true);
|
||||
}
|
||||
|
||||
void add(File asset, String assetName) {
|
||||
asset.copySync('${_assetDir.path}/$assetName');
|
||||
}
|
||||
|
||||
Directory get directory => _assetDir;
|
||||
}
|
||||
|
||||
class ApkBuilder {
|
||||
final String androidSDK;
|
||||
|
||||
File _androidJar;
|
||||
File _aapt;
|
||||
File _zipalign;
|
||||
String _jarsigner;
|
||||
|
||||
ApkBuilder(this.androidSDK) {
|
||||
_androidJar = new File('$androidSDK/platforms/android-$kAndroidPlatformVersion/android.jar');
|
||||
|
||||
String buildTools = '$androidSDK/build-tools/$kBuildToolsVersion';
|
||||
_aapt = new File('$buildTools/aapt');
|
||||
_zipalign = new File('$buildTools/zipalign');
|
||||
_jarsigner = 'jarsigner';
|
||||
}
|
||||
|
||||
void package(File androidManifest, Directory assets, File outputApk) {
|
||||
_run(_aapt.path, [
|
||||
'package',
|
||||
'-M', androidManifest.path,
|
||||
'-A', assets.path,
|
||||
'-I', _androidJar.path,
|
||||
'-F', outputApk.path,
|
||||
]);
|
||||
}
|
||||
|
||||
void add(Directory base, String resource, File outputApk) {
|
||||
_run(_aapt.path, [
|
||||
'add', '-f', outputApk.absolute.path, resource,
|
||||
], workingDirectory: base.path);
|
||||
}
|
||||
|
||||
void sign(File keystore, String keystorePassword, String keyName, File outputApk) {
|
||||
_run(_jarsigner, [
|
||||
'-keystore', keystore.path,
|
||||
'-storepass', keystorePassword,
|
||||
outputApk.path,
|
||||
keyName,
|
||||
]);
|
||||
}
|
||||
|
||||
void align(File unalignedApk, File outputApk) {
|
||||
_run(_zipalign.path, ['4', unalignedApk.path, outputApk.path]);
|
||||
}
|
||||
|
||||
void _run(String command, List<String> args, { String workingDirectory }) {
|
||||
ProcessResult result = Process.runSync(
|
||||
command, args, workingDirectory: workingDirectory);
|
||||
if (result.exitCode == 0)
|
||||
return;
|
||||
stdout.write(result.stdout);
|
||||
stderr.write(result.stderr);
|
||||
}
|
||||
}
|
||||
|
||||
main(List<String> argv) async {
|
||||
ArgParser parser = new ArgParser();
|
||||
parser.addFlag('help', abbr: 'h', negatable: false);
|
||||
parser.addOption('android-sdk');
|
||||
parser.addOption('skyx');
|
||||
|
||||
ArgResults args = parser.parse(argv);
|
||||
|
||||
if (args['help']) {
|
||||
print('usage: pub run sky_tools:build_sky_apk <options>');
|
||||
print('');
|
||||
print(parser.usage);
|
||||
return;
|
||||
}
|
||||
|
||||
Directory artifacts = new Directory('artifacts');
|
||||
File keystore = new File('${artifacts.path}/chromium-debug.keystore');
|
||||
File androidManifest = new File('${artifacts.path}/AndroidManifest.xml');
|
||||
File icuData = new File('${artifacts.path}/assets/icudtl.dat');
|
||||
File appSkyx = new File(args['skyx']);
|
||||
|
||||
Directory outDir = new Directory('out');
|
||||
outDir.createSync(recursive: true);
|
||||
|
||||
AssetBuilder assetBuilder = new AssetBuilder(outDir);
|
||||
assetBuilder.add(icuData, 'icudtl.dat');
|
||||
assetBuilder.add(appSkyx, 'app.skyx');
|
||||
|
||||
ApkBuilder builder = new ApkBuilder(args['android-sdk']);
|
||||
|
||||
File unalignedApk = new File('${outDir.path}/Example.apk.unaligned');
|
||||
File finalApk = new File('${outDir.path}/Example.apk');
|
||||
|
||||
builder.package(androidManifest, assetBuilder.directory, unalignedApk);
|
||||
builder.add(artifacts, 'classes.dex', unalignedApk);
|
||||
builder.add(artifacts, 'lib/armeabi-v7a/libsky_shell.so', unalignedApk);
|
||||
builder.sign(keystore, kKeystorePassword, kKeystoreKeyName, unalignedApk);
|
||||
builder.align(unalignedApk, finalApk);
|
||||
}
|
85
packages/flutter_tools/bin/sky_server.dart
Normal file
85
packages/flutter_tools/bin/sky_server.dart
Normal file
@ -0,0 +1,85 @@
|
||||
// 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:io';
|
||||
|
||||
import 'package:args/args.dart';
|
||||
import 'package:shelf/shelf.dart';
|
||||
import 'package:shelf/shelf_io.dart' as io;
|
||||
import 'package:shelf_route/shelf_route.dart' as shelf_route;
|
||||
import 'package:shelf_static/shelf_static.dart';
|
||||
|
||||
void printUsage(parser) {
|
||||
print('Usage: sky_server [-v] PORT');
|
||||
print(parser.usage);
|
||||
}
|
||||
|
||||
void addRoute(var router, String route, String path) {
|
||||
router.add(
|
||||
route,
|
||||
['GET', 'HEAD'],
|
||||
createStaticHandler(
|
||||
path,
|
||||
serveFilesOutsidePath: true,
|
||||
listDirectories: true
|
||||
), exactMatch: false
|
||||
);
|
||||
}
|
||||
|
||||
main(List<String> argv) async {
|
||||
ArgParser parser = new ArgParser();
|
||||
parser.addFlag('help', abbr: 'h', negatable: false,
|
||||
help: 'Display this help message.');
|
||||
parser.addFlag('verbose', abbr: 'v', negatable: false,
|
||||
help: 'Log requests to stdout.');
|
||||
parser.addOption('route', allowMultiple: true, splitCommas: false,
|
||||
help: 'Adds a virtual directory to the root.');
|
||||
|
||||
ArgResults args = parser.parse(argv);
|
||||
|
||||
if (args['help'] || args.rest.length != 1) {
|
||||
printUsage(parser);
|
||||
return;
|
||||
}
|
||||
|
||||
int port;
|
||||
try {
|
||||
port = int.parse(args.rest[0]);
|
||||
} catch(e) {
|
||||
printUsage(parser);
|
||||
return;
|
||||
}
|
||||
|
||||
var router = shelf_route.router();
|
||||
|
||||
if (args['route'] != null) {
|
||||
for (String arg in args['route']) {
|
||||
List<String> parsedArgs = arg.split(',');
|
||||
addRoute(router, parsedArgs[0], parsedArgs[1]);
|
||||
}
|
||||
}
|
||||
|
||||
addRoute(router, '/', Directory.current.path);
|
||||
|
||||
var handler = router.handler;
|
||||
|
||||
if (args['verbose'])
|
||||
handler = const Pipeline().addMiddleware(logRequests()).addHandler(handler);
|
||||
|
||||
HttpServer server;
|
||||
try {
|
||||
server = await io.serve(handler, InternetAddress.LOOPBACK_IP_V4, port);
|
||||
print('Serving ${Directory.current.absolute.path} from '
|
||||
'http://${server.address.address}:${server.port}.');
|
||||
} catch(e) {
|
||||
print(e);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
server.defaultResponseHeaders
|
||||
..removeAll('x-content-type-options')
|
||||
..removeAll('x-frame-options')
|
||||
..removeAll('x-xss-protection')
|
||||
..add('cache-control', 'no-store');
|
||||
}
|
11
packages/flutter_tools/bin/sky_test.dart
Normal file
11
packages/flutter_tools/bin/sky_test.dart
Normal file
@ -0,0 +1,11 @@
|
||||
// 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 'package:sky_tools/src/test/loader.dart' as loader;
|
||||
import 'package:test/src/executable.dart' as executable;
|
||||
|
||||
main(List<String> args) {
|
||||
loader.installHook();
|
||||
return executable.main(args);
|
||||
}
|
7
packages/flutter_tools/bin/sky_tools.dart
Normal file
7
packages/flutter_tools/bin/sky_tools.dart
Normal file
@ -0,0 +1,7 @@
|
||||
// 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 'package:sky_tools/executable.dart' as executable;
|
||||
|
||||
main(List<String> args) => executable.main(args);
|
77
packages/flutter_tools/lib/executable.dart
Normal file
77
packages/flutter_tools/lib/executable.dart
Normal file
@ -0,0 +1,77 @@
|
||||
// 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:args/command_runner.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:stack_trace/stack_trace.dart';
|
||||
|
||||
import 'src/commands/build.dart';
|
||||
import 'src/commands/cache.dart';
|
||||
import 'src/commands/daemon.dart';
|
||||
import 'src/commands/flutter_command_runner.dart';
|
||||
import 'src/commands/init.dart';
|
||||
import 'src/commands/install.dart';
|
||||
import 'src/commands/list.dart';
|
||||
import 'src/commands/listen.dart';
|
||||
import 'src/commands/logs.dart';
|
||||
import 'src/commands/run_mojo.dart';
|
||||
import 'src/commands/start.dart';
|
||||
import 'src/commands/stop.dart';
|
||||
import 'src/commands/trace.dart';
|
||||
import 'src/process.dart';
|
||||
|
||||
/// Main entry point for commands.
|
||||
///
|
||||
/// This function is intended to be used from the [flutter] command line tool.
|
||||
Future main(List<String> args) async {
|
||||
// This level can be adjusted by users through the `--verbose` option.
|
||||
Logger.root.level = Level.WARNING;
|
||||
Logger.root.onRecord.listen((LogRecord record) {
|
||||
if (record.level >= Level.WARNING) {
|
||||
stderr.writeln(record.message);
|
||||
} else {
|
||||
print(record.message);
|
||||
}
|
||||
if (record.error != null)
|
||||
stderr.writeln(record.error);
|
||||
if (record.stackTrace != null)
|
||||
stderr.writeln(record.stackTrace);
|
||||
});
|
||||
|
||||
FlutterCommandRunner runner = new FlutterCommandRunner()
|
||||
..addCommand(new BuildCommand())
|
||||
..addCommand(new CacheCommand())
|
||||
..addCommand(new DaemonCommand())
|
||||
..addCommand(new InitCommand())
|
||||
..addCommand(new InstallCommand())
|
||||
..addCommand(new ListCommand())
|
||||
..addCommand(new ListenCommand())
|
||||
..addCommand(new LogsCommand())
|
||||
..addCommand(new RunMojoCommand())
|
||||
..addCommand(new StartCommand())
|
||||
..addCommand(new StopCommand())
|
||||
..addCommand(new TraceCommand());
|
||||
|
||||
return Chain.capture(() async {
|
||||
dynamic result = await runner.run(args);
|
||||
if (result is int)
|
||||
exit(result);
|
||||
}, onError: (error, Chain chain) {
|
||||
if (error is UsageException) {
|
||||
stderr.writeln(error);
|
||||
// Argument error exit code.
|
||||
exit(64);
|
||||
} else if (error is ProcessExit) {
|
||||
// We've caught an exit code.
|
||||
exit(error.exitCode);
|
||||
} else {
|
||||
stderr.writeln(error);
|
||||
Logger.root.log(Level.SEVERE, '\nException:', null, chain.terse.toTrace());
|
||||
exit(1);
|
||||
}
|
||||
});
|
||||
}
|
120
packages/flutter_tools/lib/src/application_package.dart
Normal file
120
packages/flutter_tools/lib/src/application_package.dart
Normal file
@ -0,0 +1,120 @@
|
||||
// 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 'package:logging/logging.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
import 'artifacts.dart';
|
||||
import 'build_configuration.dart';
|
||||
|
||||
final Logger _logging = new Logger('sky_tools.application_package');
|
||||
|
||||
abstract class ApplicationPackage {
|
||||
/// Path to the actual apk or bundle.
|
||||
final String localPath;
|
||||
|
||||
/// Package ID from the Android Manifest or equivalent.
|
||||
final String id;
|
||||
|
||||
/// File name of the apk or bundle.
|
||||
final String name;
|
||||
|
||||
ApplicationPackage({
|
||||
String localPath,
|
||||
this.id
|
||||
}) : localPath = localPath, name = path.basename(localPath) {
|
||||
assert(localPath != null);
|
||||
assert(id != null);
|
||||
}
|
||||
}
|
||||
|
||||
class AndroidApk extends ApplicationPackage {
|
||||
static const String _defaultName = 'SkyShell.apk';
|
||||
static const String _defaultId = 'org.domokit.sky.shell';
|
||||
static const String _defaultLaunchActivity = '$_defaultId/$_defaultId.SkyActivity';
|
||||
|
||||
/// The path to the activity that should be launched.
|
||||
/// Defaults to 'org.domokit.sky.shell/org.domokit.sky.shell.SkyActivity'
|
||||
final String launchActivity;
|
||||
|
||||
AndroidApk({
|
||||
String localPath,
|
||||
String id: _defaultId,
|
||||
this.launchActivity: _defaultLaunchActivity
|
||||
}) : super(localPath: localPath, id: id) {
|
||||
assert(launchActivity != null);
|
||||
}
|
||||
}
|
||||
|
||||
class IOSApp extends ApplicationPackage {
|
||||
static const String _defaultName = 'SkyShell.app';
|
||||
static const String _defaultId = 'com.google.SkyShell';
|
||||
|
||||
IOSApp({
|
||||
String localPath,
|
||||
String id: _defaultId
|
||||
}) : super(localPath: localPath, id: id);
|
||||
}
|
||||
|
||||
class ApplicationPackageStore {
|
||||
final AndroidApk android;
|
||||
final IOSApp iOS;
|
||||
final IOSApp iOSSimulator;
|
||||
|
||||
ApplicationPackageStore({ this.android, this.iOS, this.iOSSimulator });
|
||||
|
||||
ApplicationPackage getPackageForPlatform(TargetPlatform platform) {
|
||||
switch (platform) {
|
||||
case TargetPlatform.android:
|
||||
return android;
|
||||
case TargetPlatform.iOS:
|
||||
return iOS;
|
||||
case TargetPlatform.iOSSimulator:
|
||||
return iOSSimulator;
|
||||
case TargetPlatform.linux:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<ApplicationPackageStore> forConfigs(List<BuildConfiguration> configs) async {
|
||||
AndroidApk android;
|
||||
IOSApp iOS;
|
||||
IOSApp iOSSimulator;
|
||||
|
||||
for (BuildConfiguration config in configs) {
|
||||
switch (config.targetPlatform) {
|
||||
case TargetPlatform.android:
|
||||
assert(android == null);
|
||||
if (config.type != BuildType.prebuilt) {
|
||||
String localPath = path.join(config.buildDir, 'apks', AndroidApk._defaultName);
|
||||
android = new AndroidApk(localPath: localPath);
|
||||
} else {
|
||||
Artifact artifact = ArtifactStore.getArtifact(
|
||||
type: ArtifactType.shell, targetPlatform: TargetPlatform.android);
|
||||
android = new AndroidApk(localPath: await ArtifactStore.getPath(artifact));
|
||||
}
|
||||
break;
|
||||
|
||||
case TargetPlatform.iOS:
|
||||
assert(iOS == null);
|
||||
assert(config.type != BuildType.prebuilt);
|
||||
iOS = new IOSApp(localPath: path.join(config.buildDir, IOSApp._defaultName));
|
||||
break;
|
||||
|
||||
case TargetPlatform.iOSSimulator:
|
||||
assert(iOSSimulator == null);
|
||||
assert(config.type != BuildType.prebuilt);
|
||||
iOSSimulator = new IOSApp(localPath: path.join(config.buildDir, IOSApp._defaultName));
|
||||
break;
|
||||
|
||||
case TargetPlatform.linux:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return new ApplicationPackageStore(android: android, iOS: iOS, iOSSimulator: iOSSimulator);
|
||||
}
|
||||
}
|
236
packages/flutter_tools/lib/src/artifacts.dart
Normal file
236
packages/flutter_tools/lib/src/artifacts.dart
Normal file
@ -0,0 +1,236 @@
|
||||
// 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:logging/logging.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
import 'build_configuration.dart';
|
||||
import 'os_utils.dart';
|
||||
|
||||
final Logger _logging = new Logger('sky_tools.artifacts');
|
||||
|
||||
const String _kShellCategory = 'shell';
|
||||
const String _kViewerCategory = 'viewer';
|
||||
|
||||
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.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 category, String platform, String revision}) {
|
||||
if (platform == 'darwin-x64') {
|
||||
// In the fullness of time, we'll have a consistent URL pattern for all of
|
||||
// our artifacts, but, for the time being, Mac OS X artifacts are stored in a
|
||||
// different cloud storage bucket.
|
||||
return 'https://storage.googleapis.com/mojo_infra/flutter/${platform}/${revision}/';
|
||||
}
|
||||
return 'https://storage.googleapis.com/mojo/sky/${category}/${platform}/${revision}/';
|
||||
}
|
||||
|
||||
enum ArtifactType {
|
||||
snapshot,
|
||||
shell,
|
||||
viewer,
|
||||
}
|
||||
|
||||
class Artifact {
|
||||
const Artifact._({
|
||||
this.name,
|
||||
this.fileName,
|
||||
this.category,
|
||||
this.type,
|
||||
this.hostPlatform,
|
||||
this.targetPlatform
|
||||
});
|
||||
|
||||
final String name;
|
||||
final String fileName;
|
||||
final String category; // TODO(abarth): Remove categories.
|
||||
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;
|
||||
}
|
||||
|
||||
String getUrl(String revision) {
|
||||
return _getCloudStorageBaseUrl(
|
||||
category: category,
|
||||
platform: platform,
|
||||
revision: revision
|
||||
) + fileName;
|
||||
}
|
||||
|
||||
// Whether the artifact needs to be marked as executable on disk.
|
||||
bool get executable => type == ArtifactType.snapshot;
|
||||
}
|
||||
|
||||
class ArtifactStore {
|
||||
static const List<Artifact> knownArtifacts = const <Artifact>[
|
||||
const Artifact._(
|
||||
name: 'Sky Shell',
|
||||
fileName: 'SkyShell.apk',
|
||||
category: _kShellCategory,
|
||||
type: ArtifactType.shell,
|
||||
targetPlatform: TargetPlatform.android
|
||||
),
|
||||
const Artifact._(
|
||||
name: 'Sky Snapshot',
|
||||
fileName: 'sky_snapshot',
|
||||
category: _kShellCategory,
|
||||
type: ArtifactType.snapshot,
|
||||
hostPlatform: HostPlatform.linux
|
||||
),
|
||||
const Artifact._(
|
||||
name: 'Sky Snapshot',
|
||||
fileName: 'sky_snapshot',
|
||||
category: _kShellCategory,
|
||||
type: ArtifactType.snapshot,
|
||||
hostPlatform: HostPlatform.mac
|
||||
),
|
||||
const Artifact._(
|
||||
name: 'Sky Viewer',
|
||||
fileName: 'sky_viewer.mojo',
|
||||
category: _kViewerCategory,
|
||||
type: ArtifactType.viewer,
|
||||
targetPlatform: TargetPlatform.android
|
||||
),
|
||||
const Artifact._(
|
||||
name: 'Sky Viewer',
|
||||
fileName: 'sky_viewer.mojo',
|
||||
category: _kViewerCategory,
|
||||
type: ArtifactType.viewer,
|
||||
targetPlatform: TargetPlatform.linux
|
||||
),
|
||||
];
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
static String packageRoot;
|
||||
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 getCloudStorageBaseUrl(String category, String platform) {
|
||||
return _getCloudStorageBaseUrl(
|
||||
category: category,
|
||||
platform: platform,
|
||||
revision: engineRevision
|
||||
);
|
||||
}
|
||||
|
||||
static Future _downloadFile(String url, File file) async {
|
||||
_logging.info('Downloading $url to ${file.path}.');
|
||||
HttpClient httpClient = new HttpClient();
|
||||
HttpClientRequest request = await httpClient.getUrl(Uri.parse(url));
|
||||
HttpClientResponse response = await request.close();
|
||||
_logging.fine('Received response statusCode=${response.statusCode}');
|
||||
if (response.statusCode != 200)
|
||||
throw new Exception(response.reasonPhrase);
|
||||
IOSink sink = file.openWrite();
|
||||
await sink.addStream(response);
|
||||
await sink.close();
|
||||
_logging.fine('Wrote file');
|
||||
}
|
||||
|
||||
static Directory _getBaseCacheDir() {
|
||||
Directory cacheDir = new Directory(path.join(packageRoot, 'sky_tools', 'cache'));
|
||||
if (!cacheDir.existsSync())
|
||||
cacheDir.createSync(recursive: true);
|
||||
return cacheDir;
|
||||
}
|
||||
|
||||
static Directory _getCacheDirForArtifact(Artifact artifact) {
|
||||
Directory baseDir = _getBaseCacheDir();
|
||||
// For now, all downloaded artifacts are release mode host binaries so use
|
||||
// a path that mirrors a local release build.
|
||||
// TODO(jamesr): Add support for more configurations.
|
||||
String config = 'Release';
|
||||
Directory artifactSpecificDir = new Directory(path.join(
|
||||
baseDir.path, 'sky_engine', engineRevision, config, artifact.platform));
|
||||
if (!artifactSpecificDir.existsSync())
|
||||
artifactSpecificDir.createSync(recursive: true);
|
||||
return artifactSpecificDir;
|
||||
}
|
||||
|
||||
static Future<String> getPath(Artifact artifact) async {
|
||||
Directory cacheDir = _getCacheDirForArtifact(artifact);
|
||||
File cachedFile = new File(path.join(cacheDir.path, artifact.fileName));
|
||||
if (!cachedFile.existsSync()) {
|
||||
print('Downloading ${artifact.name} from the cloud, one moment please...');
|
||||
await _downloadFile(artifact.getUrl(engineRevision), cachedFile);
|
||||
if (artifact.executable) {
|
||||
ProcessResult result = osUtils.makeExecutable(cachedFile);
|
||||
if (result.exitCode != 0)
|
||||
throw new Exception(result.stderr);
|
||||
}
|
||||
}
|
||||
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)));
|
||||
}
|
||||
}
|
57
packages/flutter_tools/lib/src/build_configuration.dart
Normal file
57
packages/flutter_tools/lib/src/build_configuration.dart
Normal file
@ -0,0 +1,57 @@
|
||||
// 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:io';
|
||||
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
final Logger _logging = new Logger('sky_tools.build_configuration');
|
||||
|
||||
enum BuildType {
|
||||
prebuilt,
|
||||
release,
|
||||
debug,
|
||||
}
|
||||
|
||||
enum HostPlatform {
|
||||
mac,
|
||||
linux,
|
||||
}
|
||||
|
||||
enum TargetPlatform {
|
||||
android,
|
||||
iOS,
|
||||
iOSSimulator,
|
||||
linux,
|
||||
}
|
||||
|
||||
HostPlatform getCurrentHostPlatform() {
|
||||
if (Platform.isMacOS)
|
||||
return HostPlatform.mac;
|
||||
if (Platform.isLinux)
|
||||
return HostPlatform.linux;
|
||||
_logging.warning('Unsupported host platform, defaulting to Linux');
|
||||
return HostPlatform.linux;
|
||||
}
|
||||
|
||||
class BuildConfiguration {
|
||||
BuildConfiguration.prebuilt({ this.hostPlatform, this.targetPlatform })
|
||||
: type = BuildType.prebuilt, buildDir = null;
|
||||
|
||||
BuildConfiguration.local({
|
||||
this.type,
|
||||
this.hostPlatform,
|
||||
this.targetPlatform,
|
||||
String enginePath,
|
||||
String buildPath
|
||||
}) : buildDir = path.normalize(path.join(enginePath, buildPath)) {
|
||||
assert(type == BuildType.debug || type == BuildType.release);
|
||||
}
|
||||
|
||||
final BuildType type;
|
||||
final HostPlatform hostPlatform;
|
||||
final TargetPlatform targetPlatform;
|
||||
final String buildDir;
|
||||
}
|
198
packages/flutter_tools/lib/src/commands/build.dart
Normal file
198
packages/flutter_tools/lib/src/commands/build.dart
Normal file
@ -0,0 +1,198 @@
|
||||
// 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 'dart:typed_data';
|
||||
|
||||
import 'package:archive/archive.dart';
|
||||
import 'package:flx/bundle.dart';
|
||||
import 'package:flx/signing.dart';
|
||||
import 'package:yaml/yaml.dart';
|
||||
|
||||
import '../toolchain.dart';
|
||||
import 'flutter_command.dart';
|
||||
|
||||
const String _kSnapshotKey = 'snapshot_blob.bin';
|
||||
const List<String> _kDensities = const ['drawable-xxhdpi'];
|
||||
const List<String> _kThemes = const ['white', 'black'];
|
||||
const List<int> _kSizes = const [24];
|
||||
|
||||
class _Asset {
|
||||
final String base;
|
||||
final String key;
|
||||
|
||||
_Asset({ this.base, this.key });
|
||||
}
|
||||
|
||||
Iterable<_Asset> _parseAssets(Map manifestDescriptor, String manifestPath) sync* {
|
||||
if (manifestDescriptor == null || !manifestDescriptor.containsKey('assets'))
|
||||
return;
|
||||
String basePath = new File(manifestPath).parent.path;
|
||||
for (String asset in manifestDescriptor['assets'])
|
||||
yield new _Asset(base: basePath, key: asset);
|
||||
}
|
||||
|
||||
class _MaterialAsset {
|
||||
final String name;
|
||||
final String density;
|
||||
final String theme;
|
||||
final int size;
|
||||
|
||||
_MaterialAsset(Map descriptor)
|
||||
: name = descriptor['name'],
|
||||
density = descriptor['density'],
|
||||
theme = descriptor['theme'],
|
||||
size = descriptor['size'];
|
||||
|
||||
String get key {
|
||||
List<String> parts = name.split('/');
|
||||
String category = parts[0];
|
||||
String subtype = parts[1];
|
||||
return '$category/$density/ic_${subtype}_${theme}_${size}dp.png';
|
||||
}
|
||||
}
|
||||
|
||||
List _generateValues(Map assetDescriptor, String key, List defaults) {
|
||||
if (assetDescriptor.containsKey(key))
|
||||
return [assetDescriptor[key]];
|
||||
return defaults;
|
||||
}
|
||||
|
||||
Iterable<_MaterialAsset> _generateMaterialAssets(Map assetDescriptor) sync* {
|
||||
Map currentAssetDescriptor = new Map.from(assetDescriptor);
|
||||
for (String density in _generateValues(assetDescriptor, 'density', _kDensities)) {
|
||||
currentAssetDescriptor['density'] = density;
|
||||
for (String theme in _generateValues(assetDescriptor, 'theme', _kThemes)) {
|
||||
currentAssetDescriptor['theme'] = theme;
|
||||
for (int size in _generateValues(assetDescriptor, 'size', _kSizes)) {
|
||||
currentAssetDescriptor['size'] = size;
|
||||
yield new _MaterialAsset(currentAssetDescriptor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Iterable<_MaterialAsset> _parseMaterialAssets(Map manifestDescriptor) sync* {
|
||||
if (manifestDescriptor == null || !manifestDescriptor.containsKey('material-design-icons'))
|
||||
return;
|
||||
for (Map assetDescriptor in manifestDescriptor['material-design-icons']) {
|
||||
for (_MaterialAsset asset in _generateMaterialAssets(assetDescriptor)) {
|
||||
yield asset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dynamic _loadManifest(String manifestPath) {
|
||||
if (manifestPath == null || !FileSystemEntity.isFileSync(manifestPath))
|
||||
return null;
|
||||
String manifestDescriptor = new File(manifestPath).readAsStringSync();
|
||||
return loadYaml(manifestDescriptor);
|
||||
}
|
||||
|
||||
ArchiveFile _createFile(String key, String assetBase) {
|
||||
File file = new File('${assetBase}/${key}');
|
||||
if (!file.existsSync())
|
||||
return null;
|
||||
List<int> content = file.readAsBytesSync();
|
||||
return new ArchiveFile.noCompress(key, content.length, content);
|
||||
}
|
||||
|
||||
ArchiveFile _createSnapshotFile(String snapshotPath) {
|
||||
File file = new File(snapshotPath);
|
||||
List<int> content = file.readAsBytesSync();
|
||||
return new ArchiveFile(_kSnapshotKey, content.length, content);
|
||||
}
|
||||
|
||||
const String _kDefaultAssetBase = 'packages/material_design_icons/icons';
|
||||
const String _kDefaultMainPath = 'lib/main.dart';
|
||||
const String _kDefaultManifestPath = 'flutter.yaml';
|
||||
const String _kDefaultOutputPath = 'app.flx';
|
||||
const String _kDefaultSnapshotPath = 'snapshot_blob.bin';
|
||||
const String _kDefaultPrivateKeyPath = 'privatekey.der';
|
||||
|
||||
class BuildCommand extends FlutterCommand {
|
||||
final String name = 'build';
|
||||
final String description = 'Create a Flutter app.';
|
||||
|
||||
BuildCommand() {
|
||||
argParser.addFlag('precompiled', negatable: false);
|
||||
argParser.addOption('asset-base', defaultsTo: _kDefaultAssetBase);
|
||||
argParser.addOption('compiler');
|
||||
argParser.addOption('main', defaultsTo: _kDefaultMainPath);
|
||||
argParser.addOption('manifest', defaultsTo: _kDefaultManifestPath);
|
||||
argParser.addOption('private-key', defaultsTo: _kDefaultPrivateKeyPath);
|
||||
argParser.addOption('output-file', abbr: 'o', defaultsTo: _kDefaultOutputPath);
|
||||
argParser.addOption('snapshot', defaultsTo: _kDefaultSnapshotPath);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> runInProject() async {
|
||||
String compilerPath = argResults['compiler'];
|
||||
|
||||
if (compilerPath == null)
|
||||
await downloadToolchain();
|
||||
else
|
||||
toolchain = new Toolchain(compiler: new Compiler(compilerPath));
|
||||
|
||||
return await build(
|
||||
assetBase: argResults['asset-base'],
|
||||
mainPath: argResults['main'],
|
||||
manifestPath: argResults['manifest'],
|
||||
outputPath: argResults['output-file'],
|
||||
snapshotPath: argResults['snapshot'],
|
||||
privateKeyPath: argResults['private-key'],
|
||||
precompiledSnapshot: argResults['precompiled']
|
||||
);
|
||||
}
|
||||
|
||||
Future<int> build({
|
||||
String assetBase: _kDefaultAssetBase,
|
||||
String mainPath: _kDefaultMainPath,
|
||||
String manifestPath: _kDefaultManifestPath,
|
||||
String outputPath: _kDefaultOutputPath,
|
||||
String snapshotPath: _kDefaultSnapshotPath,
|
||||
String privateKeyPath: _kDefaultPrivateKeyPath,
|
||||
bool precompiledSnapshot: false
|
||||
}) async {
|
||||
Map manifestDescriptor = _loadManifest(manifestPath);
|
||||
|
||||
Iterable<_Asset> assets = _parseAssets(manifestDescriptor, manifestPath);
|
||||
Iterable<_MaterialAsset> materialAssets = _parseMaterialAssets(manifestDescriptor);
|
||||
|
||||
Archive archive = new Archive();
|
||||
|
||||
if (!precompiledSnapshot) {
|
||||
// In a precompiled snapshot, the instruction buffer contains script
|
||||
// content equivalents
|
||||
int result = await toolchain.compiler.compile(mainPath: mainPath, snapshotPath: snapshotPath);
|
||||
if (result != 0)
|
||||
return result;
|
||||
|
||||
archive.addFile(_createSnapshotFile(snapshotPath));
|
||||
}
|
||||
|
||||
for (_Asset asset in assets)
|
||||
archive.addFile(_createFile(asset.key, asset.base));
|
||||
|
||||
for (_MaterialAsset asset in materialAssets) {
|
||||
ArchiveFile file = _createFile(asset.key, assetBase);
|
||||
if (file != null)
|
||||
archive.addFile(file);
|
||||
}
|
||||
|
||||
await CipherParameters.get().seedRandom();
|
||||
|
||||
AsymmetricKeyPair keyPair = keyPairFromPrivateKeyFileSync(privateKeyPath);
|
||||
Uint8List zipBytes = new Uint8List.fromList(new ZipEncoder().encode(archive));
|
||||
Bundle bundle = new Bundle.fromContent(
|
||||
path: outputPath,
|
||||
manifest: manifestDescriptor,
|
||||
contentBytes: zipBytes,
|
||||
keyPair: keyPair
|
||||
);
|
||||
bundle.writeSync();
|
||||
return 0;
|
||||
}
|
||||
}
|
43
packages/flutter_tools/lib/src/commands/cache.dart
Normal file
43
packages/flutter_tools/lib/src/commands/cache.dart
Normal file
@ -0,0 +1,43 @@
|
||||
// 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 'package:args/command_runner.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
import '../artifacts.dart';
|
||||
|
||||
final Logger _logging = new Logger('sky_tools.cache');
|
||||
|
||||
class CacheCommand extends Command {
|
||||
final String name = 'cache';
|
||||
final String description = 'Manages sky_tools\' cache of binary artifacts.';
|
||||
CacheCommand() {
|
||||
addSubcommand(new _ClearCommand());
|
||||
addSubcommand(new _PopulateCommand());
|
||||
}
|
||||
}
|
||||
|
||||
class _ClearCommand extends Command {
|
||||
final String name = 'clear';
|
||||
final String description = 'Clears all artifacts from the cache.';
|
||||
|
||||
@override
|
||||
Future<int> run() async {
|
||||
await ArtifactStore.clear();
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
class _PopulateCommand extends Command {
|
||||
final String name = 'populate';
|
||||
final String description = 'Populates the cache with all known artifacts.';
|
||||
|
||||
@override
|
||||
Future<int> run() async {
|
||||
await ArtifactStore.populate();
|
||||
return 0;
|
||||
}
|
||||
}
|
214
packages/flutter_tools/lib/src/commands/daemon.dart
Normal file
214
packages/flutter_tools/lib/src/commands/daemon.dart
Normal file
@ -0,0 +1,214 @@
|
||||
// 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:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
import 'flutter_command.dart';
|
||||
import 'start.dart';
|
||||
import 'stop.dart';
|
||||
|
||||
const String protocolVersion = '0.0.1';
|
||||
|
||||
/// A @domain annotation.
|
||||
const String domain = 'domain';
|
||||
|
||||
/// A domain @command annotation.
|
||||
const String command = 'command';
|
||||
|
||||
final Logger _logging = new Logger('sky_tools.daemon');
|
||||
|
||||
// TODO: Create a `device` domain in order to list devices and fire events when
|
||||
// devices are added or removed.
|
||||
|
||||
// TODO: Is this the best name? Server? Daemon?
|
||||
|
||||
/// A server process command. This command will start up a long-lived server.
|
||||
/// It reads JSON-RPC based commands from stdin, executes them, and returns
|
||||
/// JSON-RPC based responses and events to stdout.
|
||||
///
|
||||
/// It can be shutdown with a `daemon.shutdown` command (or by killing the
|
||||
/// process).
|
||||
class DaemonCommand extends FlutterCommand {
|
||||
final String name = 'daemon';
|
||||
final String description =
|
||||
'Run a persistent, JSON-RPC based server to communicate with devices.';
|
||||
final String usageFooter =
|
||||
'\nThis command is intended to be used by tooling environments that need '
|
||||
'a programatic interface into launching Flutter applications.';
|
||||
|
||||
@override
|
||||
Future<int> runInProject() async {
|
||||
print('Starting device daemon...');
|
||||
|
||||
Stream<Map> commandStream = stdin
|
||||
.transform(UTF8.decoder)
|
||||
.transform(const LineSplitter())
|
||||
.where((String line) => line.startsWith('[{') && line.endsWith('}]'))
|
||||
.map((String line) {
|
||||
line = line.substring(1, line.length - 1);
|
||||
return JSON.decode(line);
|
||||
});
|
||||
|
||||
await downloadApplicationPackagesAndConnectToDevices();
|
||||
|
||||
Daemon daemon = new Daemon(commandStream, (Map command) {
|
||||
stdout.writeln('[${JSON.encode(command)}]');
|
||||
}, daemonCommand: this);
|
||||
|
||||
return daemon.onExit;
|
||||
}
|
||||
}
|
||||
|
||||
typedef void DispatchComand(Map command);
|
||||
|
||||
typedef Future<dynamic> CommandHandler(dynamic args);
|
||||
|
||||
class Daemon {
|
||||
final DispatchComand sendCommand;
|
||||
final DaemonCommand daemonCommand;
|
||||
|
||||
final Completer<int> _onExitCompleter = new Completer();
|
||||
final Map<String, Domain> _domains = {};
|
||||
|
||||
Daemon(Stream<Map> commandStream, this.sendCommand, {this.daemonCommand}) {
|
||||
// Set up domains.
|
||||
_registerDomain(new DaemonDomain(this));
|
||||
_registerDomain(new AppDomain(this));
|
||||
|
||||
// Start listening.
|
||||
commandStream.listen(
|
||||
(Map command) => _handleCommand(command),
|
||||
onDone: () => _onExitCompleter.complete(0)
|
||||
);
|
||||
}
|
||||
|
||||
void _registerDomain(Domain domain) {
|
||||
_domains[domain.name] = domain;
|
||||
}
|
||||
|
||||
Future<int> get onExit => _onExitCompleter.future;
|
||||
|
||||
void _handleCommand(Map command) {
|
||||
// {id, event, params}
|
||||
var id = command['id'];
|
||||
|
||||
if (id == null) {
|
||||
_logging.severe('no id for command: ${command}');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
String event = command['event'];
|
||||
if (event.indexOf('.') == -1)
|
||||
throw 'command not understood: ${event}';
|
||||
|
||||
String prefix = event.substring(0, event.indexOf('.'));
|
||||
String name = event.substring(event.indexOf('.') + 1);
|
||||
if (_domains[prefix] == null)
|
||||
throw 'no domain for command: ${command}';
|
||||
|
||||
_domains[prefix].handleEvent(name, id, command['params']);
|
||||
} catch (error, trace) {
|
||||
_send({'id': id, 'error': _toJsonable(error)});
|
||||
_logging.warning('error handling ${command['event']}', error, trace);
|
||||
}
|
||||
}
|
||||
|
||||
void _send(Map map) => sendCommand(map);
|
||||
|
||||
void shutdown() {
|
||||
if (!_onExitCompleter.isCompleted)
|
||||
_onExitCompleter.complete(0);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class Domain {
|
||||
final Daemon daemon;
|
||||
final String name;
|
||||
final Map<String, CommandHandler> _handlers = {};
|
||||
|
||||
Domain(this.daemon, this.name);
|
||||
|
||||
void registerHandler(String name, CommandHandler handler) {
|
||||
_handlers[name] = handler;
|
||||
}
|
||||
|
||||
String toString() => name;
|
||||
|
||||
void handleEvent(String name, dynamic id, dynamic args) {
|
||||
new Future.sync(() {
|
||||
if (_handlers.containsKey(name))
|
||||
return _handlers[name](args);
|
||||
throw 'command not understood: ${name}';
|
||||
}).then((result) {
|
||||
if (result == null) {
|
||||
_send({'id': id});
|
||||
} else {
|
||||
_send({'id': id, 'result': _toJsonable(result)});
|
||||
}
|
||||
}).catchError((error, trace) {
|
||||
_send({'id': id, 'error': _toJsonable(error)});
|
||||
_logging.warning('error handling ${name}', error, trace);
|
||||
});
|
||||
}
|
||||
|
||||
void _send(Map map) => daemon._send(map);
|
||||
}
|
||||
|
||||
/// This domain responds to methods like [version] and [shutdown].
|
||||
@domain
|
||||
class DaemonDomain extends Domain {
|
||||
DaemonDomain(Daemon daemon) : super(daemon, 'daemon') {
|
||||
registerHandler('version', version);
|
||||
registerHandler('shutdown', shutdown);
|
||||
}
|
||||
|
||||
@command
|
||||
Future<dynamic> version(dynamic args) {
|
||||
return new Future.value(protocolVersion);
|
||||
}
|
||||
|
||||
@command
|
||||
Future<dynamic> shutdown(dynamic args) {
|
||||
Timer.run(() => daemon.shutdown());
|
||||
return new Future.value();
|
||||
}
|
||||
}
|
||||
|
||||
/// This domain responds to methods like [start] and [stopAll].
|
||||
///
|
||||
/// It'll be extended to fire events for when applications start, stop, and
|
||||
/// log data.
|
||||
@domain
|
||||
class AppDomain extends Domain {
|
||||
AppDomain(Daemon daemon) : super(daemon, 'app') {
|
||||
registerHandler('start', start);
|
||||
registerHandler('stopAll', stopAll);
|
||||
}
|
||||
|
||||
@command
|
||||
Future<dynamic> start(dynamic args) {
|
||||
// TODO: Add the ability to pass args: target, http, checked
|
||||
StartCommand startComand = new StartCommand();
|
||||
startComand.inheritFromParent(daemon.daemonCommand);
|
||||
return startComand.runInProject().then((_) => null);
|
||||
}
|
||||
|
||||
@command
|
||||
Future<bool> stopAll(dynamic args) {
|
||||
StopCommand stopCommand = new StopCommand();
|
||||
stopCommand.inheritFromParent(daemon.daemonCommand);
|
||||
return stopCommand.stop();
|
||||
}
|
||||
}
|
||||
|
||||
dynamic _toJsonable(dynamic obj) {
|
||||
if (obj is String || obj is int || obj is bool || obj is Map || obj is List || obj == null)
|
||||
return obj;
|
||||
return '${obj}';
|
||||
}
|
64
packages/flutter_tools/lib/src/commands/flutter_command.dart
Normal file
64
packages/flutter_tools/lib/src/commands/flutter_command.dart
Normal file
@ -0,0 +1,64 @@
|
||||
// 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:args/command_runner.dart';
|
||||
|
||||
import '../application_package.dart';
|
||||
import '../device.dart';
|
||||
import '../toolchain.dart';
|
||||
import 'flutter_command_runner.dart';
|
||||
|
||||
abstract class FlutterCommand extends Command {
|
||||
FlutterCommandRunner get runner => super.runner;
|
||||
|
||||
/// Whether this command needs to be run from the root of a project.
|
||||
bool get requiresProjectRoot => true;
|
||||
|
||||
Future downloadApplicationPackages() async {
|
||||
if (applicationPackages == null)
|
||||
applicationPackages = await ApplicationPackageStore.forConfigs(runner.buildConfigurations);
|
||||
}
|
||||
|
||||
Future downloadToolchain() async {
|
||||
if (toolchain == null)
|
||||
toolchain = await Toolchain.forConfigs(runner.buildConfigurations);
|
||||
}
|
||||
|
||||
void connectToDevices() {
|
||||
if (devices == null)
|
||||
devices = new DeviceStore.forConfigs(runner.buildConfigurations);
|
||||
}
|
||||
|
||||
Future downloadApplicationPackagesAndConnectToDevices() async {
|
||||
await downloadApplicationPackages();
|
||||
connectToDevices();
|
||||
}
|
||||
|
||||
void inheritFromParent(FlutterCommand other) {
|
||||
applicationPackages = other.applicationPackages;
|
||||
toolchain = other.toolchain;
|
||||
devices = other.devices;
|
||||
}
|
||||
|
||||
Future<int> run() async {
|
||||
if (requiresProjectRoot) {
|
||||
if (!FileSystemEntity.isFileSync('pubspec.yaml')) {
|
||||
stderr.writeln('No pubspec.yaml file found. '
|
||||
'This command should be run from the root of a project.');
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
return runInProject();
|
||||
}
|
||||
|
||||
Future<int> runInProject();
|
||||
|
||||
ApplicationPackageStore applicationPackages;
|
||||
Toolchain toolchain;
|
||||
DeviceStore devices;
|
||||
}
|
@ -0,0 +1,213 @@
|
||||
// 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:args/args.dart';
|
||||
import 'package:args/command_runner.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
import '../artifacts.dart';
|
||||
import '../build_configuration.dart';
|
||||
import '../process.dart';
|
||||
|
||||
final Logger _logging = new Logger('sky_tools.flutter_command_runner');
|
||||
|
||||
class FlutterCommandRunner extends CommandRunner {
|
||||
FlutterCommandRunner()
|
||||
: super('flutter', 'Manage your Flutter app development.') {
|
||||
argParser.addFlag('verbose',
|
||||
abbr: 'v',
|
||||
negatable: false,
|
||||
help: 'Noisy logging, including all shell commands executed.');
|
||||
argParser.addFlag('very-verbose',
|
||||
negatable: false,
|
||||
help: 'Very noisy logging, including the output of all '
|
||||
'shell commands executed.');
|
||||
argParser.addOption('package-root',
|
||||
help: 'Path to your packages directory.', defaultsTo: 'packages');
|
||||
|
||||
argParser.addSeparator('Local build selection options:');
|
||||
argParser.addFlag('debug',
|
||||
negatable: false,
|
||||
help:
|
||||
'Set this if you are building Flutter locally and want to use the debug build products. '
|
||||
'When set, attempts to automaticaly determine engine-src-path if engine-src-path is '
|
||||
'not set. Not normally required.');
|
||||
argParser.addFlag('release',
|
||||
negatable: false,
|
||||
help:
|
||||
'Set this if you are building Flutter locally and want to use the release build products. '
|
||||
'When set, attempts to automaticaly determine engine-src-path if engine-src-path is '
|
||||
'not set. Note that release is not compatible with the listen command '
|
||||
'on iOS devices and simulators. Not normally required.');
|
||||
argParser.addFlag('local-build',
|
||||
negatable: false,
|
||||
help:
|
||||
'Automatically detect your engine src directory from an overridden Flutter package.'
|
||||
'Useful if you are building Flutter locally and are using a dependency_override for'
|
||||
'the Flutter package that points to your engine src directory.');
|
||||
argParser.addOption('engine-src-path', hide: true,
|
||||
help:
|
||||
'Path to your engine src directory, if you are building Flutter locally. '
|
||||
'Ignored if neither debug nor release is set. Not normally required.');
|
||||
argParser.addOption('android-debug-build-path', hide: true,
|
||||
help:
|
||||
'Path to your Android Debug out directory, if you are building Flutter locally. '
|
||||
'This path is relative to engine-src-path. Not normally required.',
|
||||
defaultsTo: 'out/android_Debug/');
|
||||
argParser.addOption('android-release-build-path', hide: true,
|
||||
help:
|
||||
'Path to your Android Release out directory, if you are building Flutter locally. '
|
||||
'This path is relative to engine-src-path. Not normally required.',
|
||||
defaultsTo: 'out/android_Release/');
|
||||
argParser.addOption('ios-debug-build-path', hide: true,
|
||||
help:
|
||||
'Path to your iOS Debug out directory, if you are building Flutter locally. '
|
||||
'This path is relative to engine-src-path. Not normally required.',
|
||||
defaultsTo: 'out/ios_Debug/');
|
||||
argParser.addOption('ios-release-build-path', hide: true,
|
||||
help:
|
||||
'Path to your iOS Release out directory, if you are building Flutter locally. '
|
||||
'This path is relative to engine-src-path. Not normally required.',
|
||||
defaultsTo: 'out/ios_Release/');
|
||||
argParser.addOption('ios-sim-debug-build-path', hide: true,
|
||||
help:
|
||||
'Path to your iOS Simulator Debug out directory, if you are building Sky locally. '
|
||||
'This path is relative to engine-src-path. Not normally required.',
|
||||
defaultsTo: 'out/ios_sim_Debug/');
|
||||
argParser.addOption('ios-sim-release-build-path', hide: true,
|
||||
help:
|
||||
'Path to your iOS Simulator Release out directory, if you are building Sky locally. '
|
||||
'This path is relative to engine-src-path. Not normally required.',
|
||||
defaultsTo: 'out/ios_sim_Release/');
|
||||
}
|
||||
|
||||
List<BuildConfiguration> get buildConfigurations {
|
||||
if (_buildConfigurations == null)
|
||||
_buildConfigurations = _createBuildConfigurations(_globalResults);
|
||||
return _buildConfigurations;
|
||||
}
|
||||
List<BuildConfiguration> _buildConfigurations;
|
||||
|
||||
ArgResults _globalResults;
|
||||
|
||||
Future<int> runCommand(ArgResults globalResults) {
|
||||
if (globalResults['verbose'])
|
||||
Logger.root.level = Level.INFO;
|
||||
|
||||
if (globalResults['very-verbose'])
|
||||
Logger.root.level = Level.FINE;
|
||||
|
||||
_globalResults = globalResults;
|
||||
ArtifactStore.packageRoot = globalResults['package-root'];
|
||||
|
||||
return super.runCommand(globalResults);
|
||||
}
|
||||
|
||||
List<BuildConfiguration> _createBuildConfigurations(ArgResults globalResults) {
|
||||
if (!FileSystemEntity.isDirectorySync(ArtifactStore.packageRoot)) {
|
||||
String message = '${ArtifactStore.packageRoot} is not a valid directory.';
|
||||
if (ArtifactStore.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);
|
||||
}
|
||||
|
||||
String enginePath = globalResults['engine-src-path'];
|
||||
bool isDebug = globalResults['debug'];
|
||||
bool isRelease = globalResults['release'];
|
||||
HostPlatform hostPlatform = getCurrentHostPlatform();
|
||||
|
||||
if (enginePath == null && globalResults['local-build']) {
|
||||
Directory flutterDir = new Directory(path.join(globalResults['package-root'], 'flutter'));
|
||||
String realFlutterPath = flutterDir.resolveSymbolicLinksSync();
|
||||
|
||||
enginePath = path.dirname(path.dirname(path.dirname(path.dirname(realFlutterPath))));
|
||||
bool dirExists = FileSystemEntity.isDirectorySync(path.join(enginePath, 'out'));
|
||||
if (enginePath == '/' || enginePath.isEmpty || !dirExists) {
|
||||
stderr.writeln('Unable to detect local build in $enginePath.\n'
|
||||
'Do you have a dependency override for the flutter package?');
|
||||
throw new ProcessExit(2);
|
||||
}
|
||||
}
|
||||
|
||||
List<BuildConfiguration> configs = <BuildConfiguration>[];
|
||||
|
||||
if (enginePath == null) {
|
||||
configs.add(new BuildConfiguration.prebuilt(
|
||||
hostPlatform: hostPlatform, targetPlatform: TargetPlatform.android));
|
||||
} else {
|
||||
if (!FileSystemEntity.isDirectorySync(enginePath))
|
||||
_logging.warning('$enginePath is not a valid directory');
|
||||
|
||||
if (!isDebug && !isRelease)
|
||||
isDebug = true;
|
||||
|
||||
if (isDebug) {
|
||||
configs.add(new BuildConfiguration.local(
|
||||
type: BuildType.debug,
|
||||
hostPlatform: hostPlatform,
|
||||
targetPlatform: TargetPlatform.android,
|
||||
enginePath: enginePath,
|
||||
buildPath: globalResults['android-debug-build-path']
|
||||
));
|
||||
|
||||
if (Platform.isMacOS) {
|
||||
configs.add(new BuildConfiguration.local(
|
||||
type: BuildType.debug,
|
||||
hostPlatform: hostPlatform,
|
||||
targetPlatform: TargetPlatform.iOS,
|
||||
enginePath: enginePath,
|
||||
buildPath: globalResults['ios-debug-build-path']
|
||||
));
|
||||
|
||||
configs.add(new BuildConfiguration.local(
|
||||
type: BuildType.debug,
|
||||
hostPlatform: hostPlatform,
|
||||
targetPlatform: TargetPlatform.iOSSimulator,
|
||||
enginePath: enginePath,
|
||||
buildPath: globalResults['ios-sim-debug-build-path']
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if (isRelease) {
|
||||
configs.add(new BuildConfiguration.local(
|
||||
type: BuildType.release,
|
||||
hostPlatform: hostPlatform,
|
||||
targetPlatform: TargetPlatform.android,
|
||||
enginePath: enginePath,
|
||||
buildPath: globalResults['android-release-build-path']
|
||||
));
|
||||
|
||||
if (Platform.isMacOS) {
|
||||
configs.add(new BuildConfiguration.local(
|
||||
type: BuildType.release,
|
||||
hostPlatform: hostPlatform,
|
||||
targetPlatform: TargetPlatform.iOS,
|
||||
enginePath: enginePath,
|
||||
buildPath: globalResults['ios-release-build-path']
|
||||
));
|
||||
|
||||
configs.add(new BuildConfiguration.local(
|
||||
type: BuildType.release,
|
||||
hostPlatform: hostPlatform,
|
||||
targetPlatform: TargetPlatform.iOSSimulator,
|
||||
enginePath: enginePath,
|
||||
buildPath: globalResults['ios-sim-release-build-path']
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return configs;
|
||||
}
|
||||
}
|
170
packages/flutter_tools/lib/src/commands/init.dart
Normal file
170
packages/flutter_tools/lib/src/commands/init.dart
Normal file
@ -0,0 +1,170 @@
|
||||
// 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:args/command_runner.dart';
|
||||
import 'package:mustache4dart/mustache4dart.dart' as mustache;
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import '../process.dart';
|
||||
|
||||
class InitCommand extends Command {
|
||||
final String name = 'init';
|
||||
final String description = 'Create a new Flutter project.';
|
||||
|
||||
InitCommand() {
|
||||
argParser.addOption('out', abbr: 'o', help: 'The output directory.');
|
||||
argParser.addFlag('pub',
|
||||
defaultsTo: true,
|
||||
help: 'Whether to run pub after the project has been created.');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> run() async {
|
||||
if (!argResults.wasParsed('out')) {
|
||||
print('No option specified for the output directory.');
|
||||
print(argParser.usage);
|
||||
return 2;
|
||||
}
|
||||
|
||||
// TODO: Confirm overwrite of an existing directory with the user.
|
||||
Directory out = new Directory(argResults['out']);
|
||||
|
||||
new FlutterSimpleTemplate().generateInto(out);
|
||||
|
||||
print('');
|
||||
|
||||
String message = '''All done! To run your application:
|
||||
|
||||
\$ cd ${out.path}
|
||||
\$ flutter start --checked
|
||||
''';
|
||||
|
||||
if (argResults['pub']) {
|
||||
print("Running pub get...");
|
||||
Process process = await Process.start(
|
||||
sdkBinaryName('pub'), ['get'], workingDirectory: out.path);
|
||||
stdout.addStream(process.stdout);
|
||||
stderr.addStream(process.stderr);
|
||||
int code = await process.exitCode;
|
||||
if (code != 0) return code;
|
||||
}
|
||||
|
||||
print(message);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
abstract class Template {
|
||||
final String name;
|
||||
final String description;
|
||||
|
||||
Map<String, String> files = {};
|
||||
|
||||
Template(this.name, this.description);
|
||||
|
||||
void generateInto(Directory dir) {
|
||||
String dirPath = p.normalize(dir.absolute.path);
|
||||
String projectName = _normalizeProjectName(p.basename(dirPath));
|
||||
print('Creating ${p.basename(projectName)}...');
|
||||
dir.createSync(recursive: true);
|
||||
|
||||
files.forEach((String path, String contents) {
|
||||
Map m = {'projectName': projectName, 'description': description};
|
||||
contents = mustache.render(contents, m);
|
||||
path = path.replaceAll('/', Platform.pathSeparator);
|
||||
File file = new File(p.join(dir.path, path));
|
||||
file.parent.createSync();
|
||||
file.writeAsStringSync(contents);
|
||||
print(file.path);
|
||||
});
|
||||
}
|
||||
|
||||
String toString() => name;
|
||||
}
|
||||
|
||||
class FlutterSimpleTemplate extends Template {
|
||||
FlutterSimpleTemplate() : super('flutter-simple', 'A minimal Flutter project.') {
|
||||
files['.gitignore'] = _gitignore;
|
||||
files['pubspec.yaml'] = _pubspec;
|
||||
files['README.md'] = _readme;
|
||||
files['lib/main.dart'] = _libMain;
|
||||
}
|
||||
}
|
||||
|
||||
String _normalizeProjectName(String name) {
|
||||
name = name.replaceAll('-', '_').replaceAll(' ', '_');
|
||||
// Strip any extension (like .dart).
|
||||
if (name.contains('.')) {
|
||||
name = name.substring(0, name.indexOf('.'));
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
const String _gitignore = r'''
|
||||
.DS_Store
|
||||
.idea
|
||||
.packages
|
||||
.pub/
|
||||
build/
|
||||
packages
|
||||
pubspec.lock
|
||||
''';
|
||||
|
||||
const String _readme = r'''
|
||||
# {{projectName}}
|
||||
|
||||
{{description}}
|
||||
|
||||
## Getting Started
|
||||
|
||||
For help getting started with Flutter, view our online
|
||||
[documentation](http://flutter.io/).
|
||||
''';
|
||||
|
||||
const String _pubspec = r'''
|
||||
name: {{projectName}}
|
||||
description: {{description}}
|
||||
dependencies:
|
||||
flutter: ">=0.0.2 <0.1.0"
|
||||
dev_dependencies:
|
||||
sky_tools: any
|
||||
''';
|
||||
|
||||
const String _libMain = r'''
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
void main() {
|
||||
runApp(
|
||||
new MaterialApp(
|
||||
title: "Flutter Demo",
|
||||
routes: {
|
||||
'/': (RouteArguments args) => new FlutterDemo()
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
class FlutterDemo extends StatelessComponent {
|
||||
Widget build(BuildContext context) {
|
||||
return new Scaffold(
|
||||
toolBar: new ToolBar(
|
||||
center: new Text("Flutter Demo")
|
||||
),
|
||||
body: new Material(
|
||||
child: new Center(
|
||||
child: new Text("Hello world!")
|
||||
)
|
||||
),
|
||||
floatingActionButton: new FloatingActionButton(
|
||||
child: new Icon(
|
||||
icon: 'content/add'
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
''';
|
42
packages/flutter_tools/lib/src/commands/install.dart
Normal file
42
packages/flutter_tools/lib/src/commands/install.dart
Normal file
@ -0,0 +1,42 @@
|
||||
// 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 '../application_package.dart';
|
||||
import '../device.dart';
|
||||
import 'flutter_command.dart';
|
||||
|
||||
class InstallCommand extends FlutterCommand {
|
||||
final String name = 'install';
|
||||
final String description = 'Install Flutter apps on attached devices.';
|
||||
|
||||
InstallCommand() {
|
||||
argParser.addFlag('boot',
|
||||
help: 'Boot the iOS Simulator if it isn\'t already running.');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> runInProject() async {
|
||||
await downloadApplicationPackagesAndConnectToDevices();
|
||||
return install(boot: argResults['boot']) ? 0 : 2;
|
||||
}
|
||||
|
||||
bool install({ bool boot: false }) {
|
||||
if (boot)
|
||||
devices.iOSSimulator?.boot();
|
||||
|
||||
bool installedSomewhere = false;
|
||||
|
||||
for (Device device in devices.all) {
|
||||
ApplicationPackage package = applicationPackages.getPackageForPlatform(device.platform);
|
||||
if (package == null || !device.isConnected() || device.isAppInstalled(package))
|
||||
continue;
|
||||
if (device.installApp(package))
|
||||
installedSomewhere = true;
|
||||
}
|
||||
|
||||
return installedSomewhere;
|
||||
}
|
||||
}
|
69
packages/flutter_tools/lib/src/commands/list.dart
Normal file
69
packages/flutter_tools/lib/src/commands/list.dart
Normal file
@ -0,0 +1,69 @@
|
||||
// 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 'package:logging/logging.dart';
|
||||
|
||||
import '../device.dart';
|
||||
import 'flutter_command.dart';
|
||||
|
||||
final Logger _logging = new Logger('sky_tools.list');
|
||||
|
||||
class ListCommand extends FlutterCommand {
|
||||
final String name = 'list';
|
||||
final String description = 'List all connected devices.';
|
||||
|
||||
ListCommand() {
|
||||
argParser.addFlag('details',
|
||||
abbr: 'd',
|
||||
negatable: false,
|
||||
help: 'Log additional details about attached devices.');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> runInProject() async {
|
||||
connectToDevices();
|
||||
|
||||
bool details = argResults['details'];
|
||||
|
||||
if (details)
|
||||
print('Android Devices:');
|
||||
|
||||
for (AndroidDevice device in AndroidDevice.getAttachedDevices(devices.android)) {
|
||||
if (details) {
|
||||
print('${device.id}\t'
|
||||
'${device.modelID}\t'
|
||||
'${device.productID}\t'
|
||||
'${device.deviceCodeName}');
|
||||
} else {
|
||||
print(device.id);
|
||||
}
|
||||
}
|
||||
|
||||
if (details)
|
||||
print('iOS Devices:');
|
||||
|
||||
for (IOSDevice device in IOSDevice.getAttachedDevices(devices.iOS)) {
|
||||
if (details) {
|
||||
print('${device.id}\t${device.name}');
|
||||
} else {
|
||||
print(device.id);
|
||||
}
|
||||
}
|
||||
|
||||
if (details) {
|
||||
print('iOS Simulators:');
|
||||
}
|
||||
for (IOSSimulator device in IOSSimulator.getAttachedDevices(devices.iOSSimulator)) {
|
||||
if (details) {
|
||||
print('${device.id}\t${device.name}');
|
||||
} else {
|
||||
print(device.id);
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
130
packages/flutter_tools/lib/src/commands/listen.dart
Normal file
130
packages/flutter_tools/lib/src/commands/listen.dart
Normal file
@ -0,0 +1,130 @@
|
||||
// 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:logging/logging.dart';
|
||||
|
||||
import '../application_package.dart';
|
||||
import '../device.dart';
|
||||
import '../process.dart';
|
||||
import 'build.dart';
|
||||
import 'flutter_command.dart';
|
||||
|
||||
final Logger _logging = new Logger('sky_tools.listen');
|
||||
|
||||
class ListenCommand extends FlutterCommand {
|
||||
final String name = 'listen';
|
||||
final String description = 'Listen for changes to files and reload the running app on all connected devices.';
|
||||
List<String> watchCommand;
|
||||
|
||||
/// Only run once. Used for testing.
|
||||
bool singleRun;
|
||||
|
||||
ListenCommand({ this.singleRun: false }) {
|
||||
argParser.addFlag('checked',
|
||||
negatable: true,
|
||||
defaultsTo: true,
|
||||
help: 'Toggle Dart\'s checked mode.');
|
||||
argParser.addOption('target',
|
||||
defaultsTo: '.',
|
||||
abbr: 't',
|
||||
help: 'Target app path or filename to start.');
|
||||
}
|
||||
|
||||
static const String _localFlutterBundle = 'app.flx';
|
||||
static const String _remoteFlutterBundle = 'Documents/app.flx';
|
||||
|
||||
@override
|
||||
Future<int> runInProject() async {
|
||||
await downloadApplicationPackagesAndConnectToDevices();
|
||||
await downloadToolchain();
|
||||
|
||||
if (argResults.rest.length > 0) {
|
||||
watchCommand = _initWatchCommand(argResults.rest);
|
||||
} else {
|
||||
watchCommand = _initWatchCommand(['.']);
|
||||
}
|
||||
|
||||
while (true) {
|
||||
_logging.info('Updating running Flutter apps...');
|
||||
|
||||
BuildCommand builder = new BuildCommand();
|
||||
builder.inheritFromParent(this);
|
||||
builder.build(outputPath: _localFlutterBundle);
|
||||
|
||||
for (Device device in devices.all) {
|
||||
ApplicationPackage package = applicationPackages.getPackageForPlatform(device.platform);
|
||||
if (package == null || !device.isConnected())
|
||||
continue;
|
||||
if (device is AndroidDevice) {
|
||||
await devices.android.startServer(
|
||||
argResults['target'], true, argResults['checked'], package);
|
||||
} else if (device is IOSDevice) {
|
||||
device.pushFile(package, _localFlutterBundle, _remoteFlutterBundle);
|
||||
} else if (device is IOSSimulator) {
|
||||
// TODO(abarth): Move pushFile up to Device once Android supports
|
||||
// pushing new bundles.
|
||||
device.pushFile(package, _localFlutterBundle, _remoteFlutterBundle);
|
||||
} else {
|
||||
assert(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (singleRun || !watchDirectory())
|
||||
break;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
List<String> _initWatchCommand(List<String> directories) {
|
||||
if (Platform.isMacOS) {
|
||||
try {
|
||||
runCheckedSync(['which', 'fswatch']);
|
||||
} catch (e) {
|
||||
_logging.severe('"listen" command is only useful if you have installed '
|
||||
'fswatch on Mac. Run "brew install fswatch" to install it with '
|
||||
'homebrew.');
|
||||
return null;
|
||||
}
|
||||
return ['fswatch', '-r', '-v', '-1']..addAll(directories);
|
||||
} else if (Platform.isLinux) {
|
||||
try {
|
||||
runCheckedSync(['which', 'inotifywait']);
|
||||
} catch (e) {
|
||||
_logging.severe('"listen" command is only useful if you have installed '
|
||||
'inotifywait on Linux. Run "apt-get install inotify-tools" or '
|
||||
'equivalent to install it.');
|
||||
return null;
|
||||
}
|
||||
return [
|
||||
'inotifywait',
|
||||
'-r',
|
||||
'-e',
|
||||
// Only listen for events that matter, to avoid triggering constantly
|
||||
// from the editor watching files
|
||||
'modify,close_write,move,create,delete',
|
||||
]..addAll(directories);
|
||||
} else {
|
||||
_logging.severe('"listen" command is only available on Mac and Linux.');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
bool watchDirectory() {
|
||||
if (watchCommand == null)
|
||||
return false;
|
||||
|
||||
try {
|
||||
runCheckedSync(watchCommand);
|
||||
} catch (e) {
|
||||
_logging.warning('Watching directories failed.', e);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
38
packages/flutter_tools/lib/src/commands/logs.dart
Normal file
38
packages/flutter_tools/lib/src/commands/logs.dart
Normal file
@ -0,0 +1,38 @@
|
||||
// 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 'package:logging/logging.dart';
|
||||
|
||||
import '../device.dart';
|
||||
import 'flutter_command.dart';
|
||||
|
||||
final Logger _logging = new Logger('sky_tools.logs');
|
||||
|
||||
class LogsCommand extends FlutterCommand {
|
||||
final String name = 'logs';
|
||||
final String description = 'Show logs for running Sky apps.';
|
||||
|
||||
LogsCommand() {
|
||||
argParser.addFlag('clear',
|
||||
negatable: false,
|
||||
help: 'Clear log history before reading from logs (Android only).');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> runInProject() async {
|
||||
connectToDevices();
|
||||
|
||||
bool clear = argResults['clear'];
|
||||
|
||||
Iterable<Future<int>> results = devices.all.map(
|
||||
(Device device) => device.logs(clear: clear));
|
||||
|
||||
for (Future<int> result in results)
|
||||
await result;
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
108
packages/flutter_tools/lib/src/commands/run_mojo.dart
Normal file
108
packages/flutter_tools/lib/src/commands/run_mojo.dart
Normal file
@ -0,0 +1,108 @@
|
||||
// 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:args/command_runner.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
import '../artifacts.dart';
|
||||
import '../build_configuration.dart';
|
||||
import '../process.dart';
|
||||
|
||||
final Logger _logging = new Logger('sky_tools.run_mojo');
|
||||
|
||||
enum _MojoConfig { Debug, Release }
|
||||
|
||||
class RunMojoCommand extends Command {
|
||||
final String name = 'run_mojo';
|
||||
final String description = 'Run a Flutter app in mojo.';
|
||||
|
||||
RunMojoCommand() {
|
||||
argParser.addFlag('android', negatable: false, help: 'Run on an Android device');
|
||||
argParser.addFlag('checked', negatable: false, help: 'Run Flutter in checked mode');
|
||||
argParser.addFlag('mojo-debug', negatable: false, help: 'Use Debug build of mojo');
|
||||
argParser.addFlag('mojo-release', negatable: false, help: 'Use Release build of mojo (default)');
|
||||
|
||||
argParser.addOption('app', defaultsTo: 'app.flx');
|
||||
argParser.addOption('mojo-path', help: 'Path to directory containing mojo_shell and services for Linux and to mojo devtools from Android.');
|
||||
}
|
||||
|
||||
// TODO(abarth): Why not use path.absolute?
|
||||
String _makePathAbsolute(String relativePath) {
|
||||
File file = new File(relativePath);
|
||||
if (!file.existsSync()) {
|
||||
throw new Exception("Path \"${relativePath}\" does not exist");
|
||||
}
|
||||
return file.absolute.path;
|
||||
}
|
||||
|
||||
Future<int> _runAndroid(String devtoolsPath, _MojoConfig mojoConfig, String appPath, List<String> additionalArgs) {
|
||||
String skyViewerUrl = ArtifactStore.getCloudStorageBaseUrl('viewer', 'android-arm');
|
||||
String command = _makePathAbsolute(devtoolsPath);
|
||||
String appName = path.basename(appPath);
|
||||
String appDir = path.dirname(appPath);
|
||||
String buildFlag = mojoConfig == _MojoConfig.Debug ? '--debug' : '--release';
|
||||
List<String> cmd = [
|
||||
command,
|
||||
'--android',
|
||||
buildFlag,
|
||||
'http://app/$appName',
|
||||
'--map-origin=http://app/=$appDir',
|
||||
'--map-origin=http://sky_viewer/=$skyViewerUrl',
|
||||
'--url-mappings=mojo:sky_viewer=http://sky_viewer/sky_viewer.mojo',
|
||||
];
|
||||
if (_logging.level <= Level.INFO) {
|
||||
cmd.add('--verbose');
|
||||
if (_logging.level <= Level.FINE) {
|
||||
cmd.add('--verbose');
|
||||
}
|
||||
}
|
||||
cmd.addAll(additionalArgs);
|
||||
return runCommandAndStreamOutput(cmd);
|
||||
}
|
||||
|
||||
Future<int> _runLinux(String mojoPath, _MojoConfig mojoConfig, String appPath, List<String> additionalArgs) async {
|
||||
Artifact artifact = ArtifactStore.getArtifact(type: ArtifactType.viewer, targetPlatform: TargetPlatform.linux);
|
||||
String viewerPath = _makePathAbsolute(await ArtifactStore.getPath(artifact));
|
||||
String mojoBuildType = mojoConfig == _MojoConfig.Debug ? 'Debug' : 'Release';
|
||||
String mojoShellPath = _makePathAbsolute(path.join(mojoPath, 'out', mojoBuildType, 'mojo_shell'));
|
||||
List<String> cmd = [
|
||||
mojoShellPath,
|
||||
'file://${appPath}',
|
||||
'--url-mappings=mojo:sky_viewer=file://${viewerPath}'
|
||||
];
|
||||
cmd.addAll(additionalArgs);
|
||||
return runCommandAndStreamOutput(cmd);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> run() async {
|
||||
if (argResults['mojo-path'] == null) {
|
||||
_logging.severe('Must specify --mojo-path.');
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (argResults['mojo-debug'] && argResults['mojo-release']) {
|
||||
_logging.severe('Cannot specify both --mojo-debug and --mojo-release');
|
||||
return 1;
|
||||
}
|
||||
List<String> args = [];
|
||||
if (argResults['checked']) {
|
||||
args.add('--args-for=mojo:sky_viewer --enable-checked-mode');
|
||||
}
|
||||
String mojoPath = argResults['mojo-path'];
|
||||
_MojoConfig mojoConfig = argResults['mojo-debug'] ? _MojoConfig.Debug : _MojoConfig.Release;
|
||||
String appPath = _makePathAbsolute(argResults['app']);
|
||||
|
||||
args.addAll(argResults.rest);
|
||||
if (argResults['android']) {
|
||||
return _runAndroid(mojoPath, mojoConfig, appPath, args);
|
||||
} else {
|
||||
return _runLinux(mojoPath, mojoConfig, appPath, args);
|
||||
}
|
||||
}
|
||||
}
|
112
packages/flutter_tools/lib/src/commands/start.dart
Normal file
112
packages/flutter_tools/lib/src/commands/start.dart
Normal file
@ -0,0 +1,112 @@
|
||||
// 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:logging/logging.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
import '../application_package.dart';
|
||||
import '../device.dart';
|
||||
import 'build.dart';
|
||||
import 'flutter_command.dart';
|
||||
import 'install.dart';
|
||||
import 'stop.dart';
|
||||
|
||||
final Logger _logging = new Logger('sky_tools.start');
|
||||
const String _localBundleName = 'app.flx';
|
||||
const String _localSnapshotName = 'snapshot_blob.bin';
|
||||
|
||||
class StartCommand extends FlutterCommand {
|
||||
final String name = 'start';
|
||||
final String description = 'Start your Flutter app on attached devices.';
|
||||
|
||||
StartCommand() {
|
||||
argParser.addFlag('poke',
|
||||
negatable: false,
|
||||
help: 'Restart the connection to the server (Android only).');
|
||||
argParser.addFlag('checked',
|
||||
negatable: true,
|
||||
defaultsTo: true,
|
||||
help: 'Toggle Dart\'s checked mode.');
|
||||
argParser.addOption('target',
|
||||
defaultsTo: '.',
|
||||
abbr: 't',
|
||||
help: 'Target app path or filename to start.');
|
||||
argParser.addFlag('http',
|
||||
negatable: true,
|
||||
help: 'Use a local HTTP server to serve your app to your device.');
|
||||
argParser.addFlag('boot',
|
||||
help: 'Boot the iOS Simulator if it isn\'t already running.');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> runInProject() async {
|
||||
await Future.wait([
|
||||
downloadToolchain(),
|
||||
downloadApplicationPackagesAndConnectToDevices(),
|
||||
]);
|
||||
|
||||
bool poke = argResults['poke'];
|
||||
if (!poke) {
|
||||
StopCommand stopper = new StopCommand();
|
||||
stopper.inheritFromParent(this);
|
||||
stopper.stop();
|
||||
|
||||
// Only install if the user did not specify a poke
|
||||
InstallCommand installer = new InstallCommand();
|
||||
installer.inheritFromParent(this);
|
||||
installer.install(boot: argResults['boot']);
|
||||
}
|
||||
|
||||
bool startedSomething = false;
|
||||
|
||||
for (Device device in devices.all) {
|
||||
ApplicationPackage package = applicationPackages.getPackageForPlatform(device.platform);
|
||||
if (package == null || !device.isConnected())
|
||||
continue;
|
||||
if (device is AndroidDevice) {
|
||||
String target = path.absolute(argResults['target']);
|
||||
if (argResults['http']) {
|
||||
if (await device.startServer(target, poke, argResults['checked'], package))
|
||||
startedSomething = true;
|
||||
} else {
|
||||
String mainPath = target;
|
||||
if (FileSystemEntity.isDirectorySync(target))
|
||||
mainPath = path.join(target, 'lib', 'main.dart');
|
||||
BuildCommand builder = new BuildCommand();
|
||||
builder.inheritFromParent(this);
|
||||
|
||||
Directory tempDir = await Directory.systemTemp.createTemp('flutter_tools');
|
||||
try {
|
||||
String localBundlePath = path.join(tempDir.path, _localBundleName);
|
||||
String localSnapshotPath = path.join(tempDir.path, _localSnapshotName);
|
||||
await builder.build(
|
||||
snapshotPath: localSnapshotPath,
|
||||
outputPath: localBundlePath,
|
||||
mainPath: mainPath);
|
||||
if (device.startBundle(package, localBundlePath, poke, argResults['checked']))
|
||||
startedSomething = true;
|
||||
} finally {
|
||||
tempDir.deleteSync(recursive: true);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (await device.startApp(package))
|
||||
startedSomething = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!startedSomething) {
|
||||
if (!devices.all.any((device) => device.isConnected())) {
|
||||
_logging.severe('Unable to run application - no connected devices.');
|
||||
} else {
|
||||
_logging.severe('Unable to run application.');
|
||||
}
|
||||
}
|
||||
|
||||
return startedSomething ? 0 : 2;
|
||||
}
|
||||
}
|
38
packages/flutter_tools/lib/src/commands/stop.dart
Normal file
38
packages/flutter_tools/lib/src/commands/stop.dart
Normal file
@ -0,0 +1,38 @@
|
||||
// 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 'package:logging/logging.dart';
|
||||
|
||||
import '../application_package.dart';
|
||||
import '../device.dart';
|
||||
import 'flutter_command.dart';
|
||||
|
||||
final Logger _logging = new Logger('sky_tools.stop');
|
||||
|
||||
class StopCommand extends FlutterCommand {
|
||||
final String name = 'stop';
|
||||
final String description = 'Stop your Flutter app on all attached devices.';
|
||||
|
||||
@override
|
||||
Future<int> runInProject() async {
|
||||
await downloadApplicationPackagesAndConnectToDevices();
|
||||
return await stop() ? 0 : 2;
|
||||
}
|
||||
|
||||
Future<bool> stop() async {
|
||||
bool stoppedSomething = false;
|
||||
|
||||
for (Device device in devices.all) {
|
||||
ApplicationPackage package = applicationPackages.getPackageForPlatform(device.platform);
|
||||
if (package == null || !device.isConnected())
|
||||
continue;
|
||||
if (await device.stopApp(package))
|
||||
stoppedSomething = true;
|
||||
}
|
||||
|
||||
return stoppedSomething;
|
||||
}
|
||||
}
|
65
packages/flutter_tools/lib/src/commands/trace.dart
Normal file
65
packages/flutter_tools/lib/src/commands/trace.dart
Normal file
@ -0,0 +1,65 @@
|
||||
// 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 'package:logging/logging.dart';
|
||||
|
||||
import '../application_package.dart';
|
||||
import '../device.dart';
|
||||
import 'flutter_command.dart';
|
||||
|
||||
final Logger _logging = new Logger('sky_tools.trace');
|
||||
|
||||
class TraceCommand extends FlutterCommand {
|
||||
final String name = 'trace';
|
||||
final String description = 'Start and stop tracing a running Flutter app '
|
||||
'(Android only, requires root).\n'
|
||||
'To start a trace, wait, and then stop the trace, don\'t set any flags '
|
||||
'except (optionally) duration.\n'
|
||||
'Otherwise, specify either start or stop to manually control the trace.';
|
||||
|
||||
TraceCommand() {
|
||||
argParser.addFlag('start', negatable: false, help: 'Start tracing.');
|
||||
argParser.addFlag('stop', negatable: false, help: 'Stop tracing.');
|
||||
argParser.addOption('duration',
|
||||
defaultsTo: '10', abbr: 'd', help: 'Duration in seconds to trace.');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> runInProject() async {
|
||||
await downloadApplicationPackagesAndConnectToDevices();
|
||||
|
||||
if (!devices.android.isConnected()) {
|
||||
_logging.warning('No device connected, so no trace was completed.');
|
||||
return 1;
|
||||
}
|
||||
|
||||
ApplicationPackage androidApp = applicationPackages.android;
|
||||
|
||||
if ((!argResults['start'] && !argResults['stop']) ||
|
||||
(argResults['start'] && argResults['stop'])) {
|
||||
// Setting neither flags or both flags means do both commands and wait
|
||||
// duration seconds in between.
|
||||
devices.android.startTracing(androidApp);
|
||||
await new Future.delayed(
|
||||
new Duration(seconds: int.parse(argResults['duration'])),
|
||||
() => _stopTracing(devices.android, androidApp));
|
||||
} else if (argResults['stop']) {
|
||||
_stopTracing(devices.android, androidApp);
|
||||
} else {
|
||||
devices.android.startTracing(androidApp);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void _stopTracing(AndroidDevice android, AndroidApk androidApp) {
|
||||
String tracePath = android.stopTracing(androidApp);
|
||||
if (tracePath == null) {
|
||||
_logging.warning('No trace file saved.');
|
||||
} else {
|
||||
print('Trace file saved to $tracePath');
|
||||
}
|
||||
}
|
||||
}
|
995
packages/flutter_tools/lib/src/device.dart
Normal file
995
packages/flutter_tools/lib/src/device.dart
Normal file
@ -0,0 +1,995 @@
|
||||
// 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:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
import 'application_package.dart';
|
||||
import 'build_configuration.dart';
|
||||
import 'os_utils.dart';
|
||||
import 'process.dart';
|
||||
|
||||
final Logger _logging = new Logger('sky_tools.device');
|
||||
|
||||
abstract class Device {
|
||||
final String id;
|
||||
static Map<String, Device> _deviceCache = {};
|
||||
|
||||
factory Device._unique(String className, [String id = null]) {
|
||||
if (id == null) {
|
||||
if (className == AndroidDevice.className) {
|
||||
id = AndroidDevice.defaultDeviceID;
|
||||
} else if (className == IOSDevice.className) {
|
||||
id = IOSDevice.defaultDeviceID;
|
||||
} else if (className == IOSSimulator.className) {
|
||||
id = IOSSimulator.defaultDeviceID;
|
||||
} else {
|
||||
throw 'Attempted to create a Device of unknown type $className';
|
||||
}
|
||||
}
|
||||
|
||||
return _deviceCache.putIfAbsent(id, () {
|
||||
if (className == AndroidDevice.className) {
|
||||
final device = new AndroidDevice._(id);
|
||||
_deviceCache[id] = device;
|
||||
return device;
|
||||
} else if (className == IOSDevice.className) {
|
||||
final device = new IOSDevice._(id);
|
||||
_deviceCache[id] = device;
|
||||
return device;
|
||||
} else if (className == IOSSimulator.className) {
|
||||
final device = new IOSSimulator._(id);
|
||||
_deviceCache[id] = device;
|
||||
return device;
|
||||
} else {
|
||||
throw 'Attempted to create a Device of unknown type $className';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Device._(this.id);
|
||||
|
||||
/// Install an app package on the current device
|
||||
bool installApp(ApplicationPackage app);
|
||||
|
||||
/// Check if the device is currently connected
|
||||
bool isConnected();
|
||||
|
||||
/// Check if the current version of the given app is already installed
|
||||
bool isAppInstalled(ApplicationPackage app);
|
||||
|
||||
TargetPlatform get platform;
|
||||
|
||||
Future<int> logs({bool clear: false});
|
||||
|
||||
/// Start an app package on the current device
|
||||
Future<bool> startApp(ApplicationPackage app);
|
||||
|
||||
/// Stop an app package on the current device
|
||||
Future<bool> stopApp(ApplicationPackage app);
|
||||
}
|
||||
|
||||
class IOSDevice extends Device {
|
||||
static const String className = 'IOSDevice';
|
||||
static final String defaultDeviceID = 'default_ios_id';
|
||||
|
||||
static const String _macInstructions =
|
||||
'To work with iOS devices, please install ideviceinstaller. '
|
||||
'If you use homebrew, you can install it with '
|
||||
'"\$ brew install ideviceinstaller".';
|
||||
static const String _linuxInstructions =
|
||||
'To work with iOS devices, please install ideviceinstaller. '
|
||||
'On Ubuntu or Debian, you can install it with '
|
||||
'"\$ apt-get install ideviceinstaller".';
|
||||
|
||||
String _installerPath;
|
||||
String get installerPath => _installerPath;
|
||||
|
||||
String _listerPath;
|
||||
String get listerPath => _listerPath;
|
||||
|
||||
String _informerPath;
|
||||
String get informerPath => _informerPath;
|
||||
|
||||
String _debuggerPath;
|
||||
String get debuggerPath => _debuggerPath;
|
||||
|
||||
String _loggerPath;
|
||||
String get loggerPath => _loggerPath;
|
||||
|
||||
String _pusherPath;
|
||||
String get pusherPath => _pusherPath;
|
||||
|
||||
String _name;
|
||||
String get name => _name;
|
||||
|
||||
factory IOSDevice({String id, String name}) {
|
||||
IOSDevice device = new Device._unique(className, id);
|
||||
device._name = name;
|
||||
return device;
|
||||
}
|
||||
|
||||
IOSDevice._(String id) : super._(id) {
|
||||
_installerPath = _checkForCommand('ideviceinstaller');
|
||||
_listerPath = _checkForCommand('idevice_id');
|
||||
_informerPath = _checkForCommand('ideviceinfo');
|
||||
_debuggerPath = _checkForCommand('idevicedebug');
|
||||
_loggerPath = _checkForCommand('idevicesyslog');
|
||||
_pusherPath = _checkForCommand(
|
||||
'ios-deploy',
|
||||
'To copy files to iOS devices, please install ios-deploy. '
|
||||
'You can do this using homebrew as follows:\n'
|
||||
'\$ brew tap flutter/flutter\n'
|
||||
'\$ brew install ios-deploy',
|
||||
'Copying files to iOS devices is not currently supported on Linux.');
|
||||
}
|
||||
|
||||
static List<IOSDevice> getAttachedDevices([IOSDevice mockIOS]) {
|
||||
List<IOSDevice> devices = [];
|
||||
for (String id in _getAttachedDeviceIDs(mockIOS)) {
|
||||
String name = _getDeviceName(id, mockIOS);
|
||||
devices.add(new IOSDevice(id: id, name: name));
|
||||
}
|
||||
return devices;
|
||||
}
|
||||
|
||||
static Iterable<String> _getAttachedDeviceIDs([IOSDevice mockIOS]) {
|
||||
String listerPath =
|
||||
(mockIOS != null) ? mockIOS.listerPath : _checkForCommand('idevice_id');
|
||||
String output;
|
||||
try {
|
||||
output = runSync([listerPath, '-l']);
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
return output.trim()
|
||||
.split('\n')
|
||||
.where((String s) => s != null && s.length > 0);
|
||||
}
|
||||
|
||||
static String _getDeviceName(String deviceID, [IOSDevice mockIOS]) {
|
||||
String informerPath = (mockIOS != null)
|
||||
? mockIOS.informerPath
|
||||
: _checkForCommand('ideviceinfo');
|
||||
return runSync([informerPath, '-k', 'DeviceName', '-u', deviceID]);
|
||||
}
|
||||
|
||||
static final Map<String, String> _commandMap = {};
|
||||
static String _checkForCommand(String command,
|
||||
[String macInstructions = _macInstructions,
|
||||
String linuxInstructions = _linuxInstructions]) {
|
||||
return _commandMap.putIfAbsent(command, () {
|
||||
try {
|
||||
command = runCheckedSync(['which', command]).trim();
|
||||
} catch (e) {
|
||||
if (Platform.isMacOS) {
|
||||
_logging.severe(macInstructions);
|
||||
} else if (Platform.isLinux) {
|
||||
_logging.severe(linuxInstructions);
|
||||
} else {
|
||||
_logging.severe('$command is not available on your platform.');
|
||||
}
|
||||
}
|
||||
return command;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
bool installApp(ApplicationPackage app) {
|
||||
try {
|
||||
if (id == defaultDeviceID) {
|
||||
runCheckedSync([installerPath, '-i', app.localPath]);
|
||||
} else {
|
||||
runCheckedSync([installerPath, '-u', id, '-i', app.localPath]);
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
bool isConnected() {
|
||||
Iterable<String> ids = _getAttachedDeviceIDs();
|
||||
for (String id in ids) {
|
||||
if (id == this.id || this.id == defaultDeviceID) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
bool isAppInstalled(ApplicationPackage app) {
|
||||
try {
|
||||
String apps = runCheckedSync([installerPath, '-l']);
|
||||
if (new RegExp(app.id, multiLine: true).hasMatch(apps)) {
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> startApp(ApplicationPackage app) async {
|
||||
if (!isAppInstalled(app)) {
|
||||
return false;
|
||||
}
|
||||
// idevicedebug hangs forever after launching the app, so kill it after
|
||||
// giving it plenty of time to send the launch command.
|
||||
return runAndKill(
|
||||
[debuggerPath, 'run', app.id], new Duration(seconds: 3)).then(
|
||||
(_) {
|
||||
return true;
|
||||
}, onError: (e) {
|
||||
_logging.info('Failure running $debuggerPath: ', e);
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> stopApp(ApplicationPackage app) async {
|
||||
// Currently we don't have a way to stop an app running on iOS.
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<bool> pushFile(
|
||||
ApplicationPackage app, String localFile, String targetFile) async {
|
||||
if (Platform.isMacOS) {
|
||||
runSync([
|
||||
pusherPath,
|
||||
'-t',
|
||||
'1',
|
||||
'--bundle_id',
|
||||
app.id,
|
||||
'--upload',
|
||||
localFile,
|
||||
'--to',
|
||||
targetFile
|
||||
]);
|
||||
return true;
|
||||
} else {
|
||||
// TODO(iansf): It may be possible to make this work on Linux. Since this
|
||||
// functionality appears to be the only that prevents us from
|
||||
// supporting iOS on Linux, it may be worth putting some time
|
||||
// into investigating this.
|
||||
// See https://bbs.archlinux.org/viewtopic.php?id=192655
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
TargetPlatform get platform => TargetPlatform.iOS;
|
||||
|
||||
/// Note that clear is not supported on iOS at this time.
|
||||
Future<int> logs({bool clear: false}) async {
|
||||
if (!isConnected()) {
|
||||
return 2;
|
||||
}
|
||||
return runCommandAndStreamOutput([loggerPath],
|
||||
prefix: 'iOS dev: ', filter: new RegExp(r'.*SkyShell.*'));
|
||||
}
|
||||
}
|
||||
|
||||
class IOSSimulator extends Device {
|
||||
static const String className = 'IOSSimulator';
|
||||
static final String defaultDeviceID = 'default_ios_sim_id';
|
||||
|
||||
static const String _macInstructions =
|
||||
'To work with iOS devices, please install ideviceinstaller. '
|
||||
'If you use homebrew, you can install it with '
|
||||
'"\$ brew install ideviceinstaller".';
|
||||
|
||||
static String _xcrunPath = path.join('/usr', 'bin', 'xcrun');
|
||||
|
||||
String _iOSSimPath;
|
||||
String get iOSSimPath => _iOSSimPath;
|
||||
|
||||
String get xcrunPath => _xcrunPath;
|
||||
|
||||
String _name;
|
||||
String get name => _name;
|
||||
|
||||
factory IOSSimulator({String id, String name, String iOSSimulatorPath}) {
|
||||
IOSSimulator device = new Device._unique(className, id);
|
||||
device._name = name;
|
||||
if (iOSSimulatorPath == null) {
|
||||
iOSSimulatorPath = path.join('/Applications', 'iOS Simulator.app',
|
||||
'Contents', 'MacOS', 'iOS Simulator');
|
||||
}
|
||||
device._iOSSimPath = iOSSimulatorPath;
|
||||
return device;
|
||||
}
|
||||
|
||||
IOSSimulator._(String id) : super._(id) {}
|
||||
|
||||
static String _getRunningSimulatorID([IOSSimulator mockIOS]) {
|
||||
String xcrunPath = mockIOS != null ? mockIOS.xcrunPath : _xcrunPath;
|
||||
String output = runCheckedSync([xcrunPath, 'simctl', 'list', 'devices']);
|
||||
|
||||
Match match;
|
||||
Iterable<Match> matches = new RegExp(r'[^\(]+\(([^\)]+)\) \(Booted\)',
|
||||
multiLine: true).allMatches(output);
|
||||
if (matches.length > 1) {
|
||||
// More than one simulator is listed as booted, which is not allowed but
|
||||
// sometimes happens erroneously. Kill them all because we don't know
|
||||
// which one is actually running.
|
||||
_logging.warning('Multiple running simulators were detected, '
|
||||
'which is not supposed to happen.');
|
||||
for (Match m in matches) {
|
||||
if (m.groupCount > 0) {
|
||||
_logging.warning('Killing simulator ${m.group(1)}');
|
||||
runSync([xcrunPath, 'simctl', 'shutdown', m.group(1)]);
|
||||
}
|
||||
}
|
||||
} else if (matches.length == 1) {
|
||||
match = matches.first;
|
||||
}
|
||||
|
||||
if (match != null && match.groupCount > 0) {
|
||||
return match.group(1);
|
||||
} else {
|
||||
_logging.info('No running simulators found');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
String _getSimulatorPath() {
|
||||
String deviceID = id == defaultDeviceID ? _getRunningSimulatorID() : id;
|
||||
String homeDirectory = path.absolute(Platform.environment['HOME']);
|
||||
if (deviceID == null) {
|
||||
return null;
|
||||
}
|
||||
return path.join(homeDirectory, 'Library', 'Developer', 'CoreSimulator',
|
||||
'Devices', deviceID);
|
||||
}
|
||||
|
||||
String _getSimulatorAppHomeDirectory(ApplicationPackage app) {
|
||||
String simulatorPath = _getSimulatorPath();
|
||||
if (simulatorPath == null) {
|
||||
return null;
|
||||
}
|
||||
return path.join(simulatorPath, 'data');
|
||||
}
|
||||
|
||||
static List<IOSSimulator> getAttachedDevices([IOSSimulator mockIOS]) {
|
||||
List<IOSSimulator> devices = [];
|
||||
String id = _getRunningSimulatorID(mockIOS);
|
||||
if (id != null) {
|
||||
// TODO(iansf): get the simulator's name
|
||||
// String name = _getDeviceName(id, mockIOS);
|
||||
devices.add(new IOSSimulator(id: id));
|
||||
}
|
||||
return devices;
|
||||
}
|
||||
|
||||
Future<bool> boot() async {
|
||||
if (!Platform.isMacOS) {
|
||||
return false;
|
||||
}
|
||||
if (isConnected()) {
|
||||
return true;
|
||||
}
|
||||
if (id == defaultDeviceID) {
|
||||
runDetached([iOSSimPath]);
|
||||
|
||||
Future<bool> checkConnection([int attempts = 20]) async {
|
||||
if (attempts == 0) {
|
||||
_logging.info('Timed out waiting for iOS Simulator $id to boot.');
|
||||
return false;
|
||||
}
|
||||
if (!isConnected()) {
|
||||
_logging.info('Waiting for iOS Simulator $id to boot...');
|
||||
return new Future.delayed(new Duration(milliseconds: 500),
|
||||
() => checkConnection(attempts - 1));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return checkConnection();
|
||||
} else {
|
||||
try {
|
||||
runCheckedSync([xcrunPath, 'simctl', 'boot', id]);
|
||||
} catch (e) {
|
||||
_logging.warning('Unable to boot iOS Simulator $id: ', e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
bool installApp(ApplicationPackage app) {
|
||||
if (!isConnected()) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
if (id == defaultDeviceID) {
|
||||
runCheckedSync([xcrunPath, 'simctl', 'install', 'booted', app.localPath]);
|
||||
} else {
|
||||
runCheckedSync([xcrunPath, 'simctl', 'install', id, app.localPath]);
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool isConnected() {
|
||||
if (!Platform.isMacOS) {
|
||||
return false;
|
||||
}
|
||||
String simulatorID = _getRunningSimulatorID();
|
||||
if (simulatorID == null) {
|
||||
return false;
|
||||
} else if (id == defaultDeviceID) {
|
||||
return true;
|
||||
} else {
|
||||
return _getRunningSimulatorID() == id;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool isAppInstalled(ApplicationPackage app) {
|
||||
try {
|
||||
String simulatorHomeDirectory = _getSimulatorAppHomeDirectory(app);
|
||||
return FileSystemEntity.isDirectorySync(simulatorHomeDirectory);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> startApp(ApplicationPackage app) async {
|
||||
if (!isAppInstalled(app)) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
if (id == defaultDeviceID) {
|
||||
runCheckedSync(
|
||||
[xcrunPath, 'simctl', 'launch', 'booted', app.id]);
|
||||
} else {
|
||||
runCheckedSync([xcrunPath, 'simctl', 'launch', id, app.id]);
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> stopApp(ApplicationPackage app) async {
|
||||
// Currently we don't have a way to stop an app running on iOS.
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<bool> pushFile(
|
||||
ApplicationPackage app, String localFile, String targetFile) async {
|
||||
if (Platform.isMacOS) {
|
||||
String simulatorHomeDirectory = _getSimulatorAppHomeDirectory(app);
|
||||
runCheckedSync(
|
||||
['cp', localFile, path.join(simulatorHomeDirectory, targetFile)]);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
TargetPlatform get platform => TargetPlatform.iOSSimulator;
|
||||
|
||||
Future<int> logs({bool clear: false}) async {
|
||||
if (!isConnected()) {
|
||||
return 2;
|
||||
}
|
||||
String homeDirectory = path.absolute(Platform.environment['HOME']);
|
||||
String simulatorDeviceID = _getRunningSimulatorID();
|
||||
String logFilePath = path.join(homeDirectory, 'Library', 'Logs',
|
||||
'CoreSimulator', simulatorDeviceID, 'system.log');
|
||||
if (clear) {
|
||||
runSync(['rm', logFilePath]);
|
||||
}
|
||||
return runCommandAndStreamOutput(['tail', '-f', logFilePath],
|
||||
prefix: 'iOS sim: ', filter: new RegExp(r'.*SkyShell.*'));
|
||||
}
|
||||
}
|
||||
|
||||
class AndroidDevice extends Device {
|
||||
static const String _ADB_PATH = 'adb';
|
||||
static const int _observatoryPort = 8181;
|
||||
static const int _serverPort = 9888;
|
||||
|
||||
static const String className = 'AndroidDevice';
|
||||
static final String defaultDeviceID = 'default_android_device';
|
||||
|
||||
static const String _kFlutterServerStartMessage = 'Serving';
|
||||
static const Duration _kFlutterServerTimeout = const Duration(seconds: 3);
|
||||
|
||||
String productID;
|
||||
String modelID;
|
||||
String deviceCodeName;
|
||||
|
||||
String _adbPath;
|
||||
String get adbPath => _adbPath;
|
||||
bool _hasAdb = false;
|
||||
bool _hasValidAndroid = false;
|
||||
|
||||
factory AndroidDevice(
|
||||
{String id: null,
|
||||
String productID: null,
|
||||
String modelID: null,
|
||||
String deviceCodeName: null}) {
|
||||
AndroidDevice device = new Device._unique(className, id);
|
||||
device.productID = productID;
|
||||
device.modelID = modelID;
|
||||
device.deviceCodeName = deviceCodeName;
|
||||
return device;
|
||||
}
|
||||
|
||||
/// mockAndroid argument is only to facilitate testing with mocks, so that
|
||||
/// we don't have to rely on the test setup having adb available to it.
|
||||
static List<AndroidDevice> getAttachedDevices([AndroidDevice mockAndroid]) {
|
||||
List<AndroidDevice> devices = [];
|
||||
String adbPath =
|
||||
(mockAndroid != null) ? mockAndroid.adbPath : _getAdbPath();
|
||||
List<String> output =
|
||||
runSync([adbPath, 'devices', '-l']).trim().split('\n');
|
||||
RegExp deviceInfo = new RegExp(
|
||||
r'^(\S+)\s+device\s+\S+\s+product:(\S+)\s+model:(\S+)\s+device:(\S+)$');
|
||||
// Skip first line, which is always 'List of devices attached'.
|
||||
for (String line in output.skip(1)) {
|
||||
Match match = deviceInfo.firstMatch(line);
|
||||
if (match != null) {
|
||||
String deviceID = match[1];
|
||||
String productID = match[2];
|
||||
String modelID = match[3];
|
||||
String deviceCodeName = match[4];
|
||||
|
||||
devices.add(new AndroidDevice(
|
||||
id: deviceID,
|
||||
productID: productID,
|
||||
modelID: modelID,
|
||||
deviceCodeName: deviceCodeName));
|
||||
} else {
|
||||
_logging.warning('Unexpected failure parsing device information '
|
||||
'from adb output:\n$line\n'
|
||||
'Please report a bug at http://flutter.io/');
|
||||
}
|
||||
}
|
||||
return devices;
|
||||
}
|
||||
|
||||
AndroidDevice._(id) : super._(id) {
|
||||
_adbPath = _getAdbPath();
|
||||
_hasAdb = _checkForAdb();
|
||||
|
||||
// Checking for lollipop only needs to be done if we are starting an
|
||||
// app, but it has an important side effect, which is to discard any
|
||||
// progress messages if the adb server is restarted.
|
||||
_hasValidAndroid = _checkForLollipopOrLater();
|
||||
|
||||
if (!_hasAdb || !_hasValidAndroid) {
|
||||
_logging.warning('Unable to run on Android.');
|
||||
}
|
||||
}
|
||||
|
||||
static String _getAdbPath() {
|
||||
if (Platform.environment.containsKey('ANDROID_HOME')) {
|
||||
String androidHomeDir = Platform.environment['ANDROID_HOME'];
|
||||
String adbPath1 =
|
||||
path.join(androidHomeDir, 'sdk', 'platform-tools', 'adb');
|
||||
String adbPath2 = path.join(androidHomeDir, 'platform-tools', 'adb');
|
||||
if (FileSystemEntity.isFileSync(adbPath1)) {
|
||||
return adbPath1;
|
||||
} else if (FileSystemEntity.isFileSync(adbPath2)) {
|
||||
return adbPath2;
|
||||
} else {
|
||||
_logging.info('"adb" not found at\n "$adbPath1" or\n "$adbPath2"\n' +
|
||||
'using default path "$_ADB_PATH"');
|
||||
return _ADB_PATH;
|
||||
}
|
||||
} else {
|
||||
return _ADB_PATH;
|
||||
}
|
||||
}
|
||||
|
||||
bool _isValidAdbVersion(String adbVersion) {
|
||||
// Sample output: 'Android Debug Bridge version 1.0.31'
|
||||
Match versionFields =
|
||||
new RegExp(r'(\d+)\.(\d+)\.(\d+)').firstMatch(adbVersion);
|
||||
if (versionFields != null) {
|
||||
int majorVersion = int.parse(versionFields[1]);
|
||||
int minorVersion = int.parse(versionFields[2]);
|
||||
int patchVersion = int.parse(versionFields[3]);
|
||||
if (majorVersion > 1) {
|
||||
return true;
|
||||
}
|
||||
if (majorVersion == 1 && minorVersion > 0) {
|
||||
return true;
|
||||
}
|
||||
if (majorVersion == 1 && minorVersion == 0 && patchVersion >= 32) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
_logging.warning(
|
||||
'Unrecognized adb version string $adbVersion. Skipping version check.');
|
||||
return true;
|
||||
}
|
||||
|
||||
bool _checkForAdb() {
|
||||
try {
|
||||
String adbVersion = runCheckedSync([adbPath, 'version']);
|
||||
if (_isValidAdbVersion(adbVersion)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
String locatedAdbPath = runCheckedSync(['which', 'adb']);
|
||||
_logging.severe('"$locatedAdbPath" is too old. '
|
||||
'Please install version 1.0.32 or later.\n'
|
||||
'Try setting ANDROID_HOME to the path to your Android SDK install. '
|
||||
'Android builds are unavailable.');
|
||||
} catch (e, stack) {
|
||||
_logging.severe('"adb" not found in \$PATH. '
|
||||
'Please install the Android SDK or set ANDROID_HOME '
|
||||
'to the path of your Android SDK install.');
|
||||
_logging.info(e);
|
||||
_logging.info(stack);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool _checkForLollipopOrLater() {
|
||||
try {
|
||||
// If the server is automatically restarted, then we get irrelevant
|
||||
// output lines like this, which we want to ignore:
|
||||
// adb server is out of date. killing..
|
||||
// * daemon started successfully *
|
||||
runCheckedSync([adbPath, 'start-server']);
|
||||
|
||||
String ready = runSync([adbPath, 'shell', 'echo', 'ready']);
|
||||
if (ready.trim() != 'ready') {
|
||||
_logging.info('Android device not found.');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Sample output: '22'
|
||||
String sdkVersion =
|
||||
runCheckedSync([adbPath, 'shell', 'getprop', 'ro.build.version.sdk'])
|
||||
.trimRight();
|
||||
|
||||
int sdkVersionParsed =
|
||||
int.parse(sdkVersion, onError: (String source) => null);
|
||||
if (sdkVersionParsed == null) {
|
||||
_logging.severe('Unexpected response from getprop: "$sdkVersion"');
|
||||
return false;
|
||||
}
|
||||
if (sdkVersionParsed < 19) {
|
||||
_logging.severe('Version "$sdkVersion" of the Android SDK is too old. '
|
||||
'Please install Jelly Bean (version 19) or later.');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
_logging.severe('Unexpected failure from adb: ', e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
String _getDeviceDataPath(ApplicationPackage app) {
|
||||
return '/data/data/${app.id}';
|
||||
}
|
||||
|
||||
String _getDeviceSha1Path(ApplicationPackage app) {
|
||||
return '${_getDeviceDataPath(app)}/${app.name}.sha1';
|
||||
}
|
||||
|
||||
String _getDeviceBundlePath(ApplicationPackage app) {
|
||||
return '${_getDeviceDataPath(app)}/dev.flx';
|
||||
}
|
||||
|
||||
String _getDeviceApkSha1(ApplicationPackage app) {
|
||||
return runCheckedSync([adbPath, 'shell', 'cat', _getDeviceSha1Path(app)]);
|
||||
}
|
||||
|
||||
String _getSourceSha1(ApplicationPackage app) {
|
||||
var sha1 = new SHA1();
|
||||
var file = new File(app.localPath);
|
||||
sha1.add(file.readAsBytesSync());
|
||||
return CryptoUtils.bytesToHex(sha1.close());
|
||||
}
|
||||
|
||||
/**
|
||||
* Since Window's paths have backslashes, we need to convert those to forward slashes to make a valid URL
|
||||
*/
|
||||
String _convertToURL(String path) {
|
||||
return path.replaceAll('\\', '/');
|
||||
}
|
||||
|
||||
@override
|
||||
bool isAppInstalled(ApplicationPackage app) {
|
||||
if (!isConnected()) {
|
||||
return false;
|
||||
}
|
||||
if (runCheckedSync([adbPath, 'shell', 'pm', 'path', app.id]) ==
|
||||
'') {
|
||||
_logging.info(
|
||||
'TODO(iansf): move this log to the caller. ${app.name} is not on the device. Installing now...');
|
||||
return false;
|
||||
}
|
||||
if (_getDeviceApkSha1(app) != _getSourceSha1(app)) {
|
||||
_logging.info(
|
||||
'TODO(iansf): move this log to the caller. ${app.name} is out of date. Installing now...');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
bool installApp(ApplicationPackage app) {
|
||||
if (!isConnected()) {
|
||||
_logging.info('Android device not connected. Not installing.');
|
||||
return false;
|
||||
}
|
||||
if (!FileSystemEntity.isFileSync(app.localPath)) {
|
||||
_logging.severe('"${app.localPath}" does not exist.');
|
||||
return false;
|
||||
}
|
||||
|
||||
print('Installing ${app.name} on device.');
|
||||
runCheckedSync([adbPath, 'install', '-r', app.localPath]);
|
||||
runCheckedSync([adbPath, 'shell', 'run-as', app.id, 'chmod', '777', _getDeviceDataPath(app)]);
|
||||
runCheckedSync([adbPath, 'shell', 'echo', '-n', _getSourceSha1(app), '>', _getDeviceSha1Path(app)]);
|
||||
return true;
|
||||
}
|
||||
|
||||
void _forwardObservatoryPort() {
|
||||
// Set up port forwarding for observatory.
|
||||
String portString = 'tcp:$_observatoryPort';
|
||||
runCheckedSync([adbPath, 'forward', portString, portString]);
|
||||
}
|
||||
|
||||
bool startBundle(AndroidApk apk, String bundlePath, bool poke, bool checked) {
|
||||
if (!FileSystemEntity.isFileSync(bundlePath)) {
|
||||
_logging.severe('Cannot find $bundlePath');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!poke)
|
||||
_forwardObservatoryPort();
|
||||
|
||||
String deviceTmpPath = '/data/local/tmp/dev.flx';
|
||||
String deviceBundlePath = _getDeviceBundlePath(apk);
|
||||
runCheckedSync([adbPath, 'push', bundlePath, deviceTmpPath]);
|
||||
runCheckedSync([adbPath, 'shell', 'mv', deviceTmpPath, deviceBundlePath]);
|
||||
List<String> cmd = [
|
||||
adbPath,
|
||||
'shell', 'am', 'start',
|
||||
'-a', 'android.intent.action.RUN',
|
||||
'-d', deviceBundlePath,
|
||||
];
|
||||
if (checked)
|
||||
cmd.addAll(['--ez', 'enable-checked-mode', 'true']);
|
||||
cmd.add(apk.launchActivity);
|
||||
runCheckedSync(cmd);
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<bool> startServer(
|
||||
String target, bool poke, bool checked, AndroidApk apk) async {
|
||||
String serverRoot = '';
|
||||
String mainDart = '';
|
||||
String missingMessage = '';
|
||||
if (FileSystemEntity.isDirectorySync(target)) {
|
||||
serverRoot = target;
|
||||
mainDart = path.join(serverRoot, 'lib', 'main.dart');
|
||||
missingMessage = 'Missing lib/main.dart in project: $serverRoot';
|
||||
} else {
|
||||
serverRoot = Directory.current.path;
|
||||
mainDart = target;
|
||||
missingMessage = '$mainDart does not exist.';
|
||||
}
|
||||
|
||||
if (!FileSystemEntity.isFileSync(mainDart)) {
|
||||
_logging.severe(missingMessage);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!poke) {
|
||||
_forwardObservatoryPort();
|
||||
|
||||
// Actually start the server.
|
||||
Process server = await Process.start(
|
||||
sdkBinaryName('pub'), ['run', 'sky_tools:sky_server', _serverPort.toString()],
|
||||
workingDirectory: serverRoot,
|
||||
mode: ProcessStartMode.DETACHED_WITH_STDIO
|
||||
);
|
||||
await server.stdout.transform(UTF8.decoder)
|
||||
.firstWhere((String value) => value.startsWith(_kFlutterServerStartMessage))
|
||||
.timeout(_kFlutterServerTimeout);
|
||||
|
||||
// Set up reverse port-forwarding so that the Android app can reach the
|
||||
// server running on localhost.
|
||||
String serverPortString = 'tcp:$_serverPort';
|
||||
runCheckedSync([adbPath, 'reverse', serverPortString, serverPortString]);
|
||||
}
|
||||
|
||||
String relativeDartMain = _convertToURL(path.relative(mainDart, from: serverRoot));
|
||||
String url = 'http://localhost:$_serverPort/$relativeDartMain';
|
||||
if (poke)
|
||||
url += '?rand=${new Random().nextDouble()}';
|
||||
|
||||
// Actually launch the app on Android.
|
||||
List<String> cmd = [
|
||||
adbPath,
|
||||
'shell', 'am', 'start',
|
||||
'-a', 'android.intent.action.VIEW',
|
||||
'-d', url,
|
||||
];
|
||||
if (checked)
|
||||
cmd.addAll(['--ez', 'enable-checked-mode', 'true']);
|
||||
cmd.add(apk.launchActivity);
|
||||
runCheckedSync(cmd);
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> startApp(ApplicationPackage app) async {
|
||||
// Android currently has to be started with startServer(...).
|
||||
assert(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<bool> stopApp(ApplicationPackage app) async {
|
||||
final AndroidApk apk = app;
|
||||
|
||||
// Turn off reverse port forwarding
|
||||
runSync([adbPath, 'reverse', '--remove', 'tcp:$_serverPort']);
|
||||
// Stop the app
|
||||
runSync([adbPath, 'shell', 'am', 'force-stop', apk.id]);
|
||||
|
||||
// Kill the server
|
||||
osUtils.killTcpPortListeners(_serverPort);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
TargetPlatform get platform => TargetPlatform.android;
|
||||
|
||||
void clearLogs() {
|
||||
runSync([adbPath, 'logcat', '-c']);
|
||||
}
|
||||
|
||||
Future<int> logs({bool clear: false}) async {
|
||||
if (!isConnected()) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
if (clear) {
|
||||
clearLogs();
|
||||
}
|
||||
|
||||
return runCommandAndStreamOutput([
|
||||
adbPath,
|
||||
'logcat',
|
||||
'-v',
|
||||
'tag', // Only log the tag and the message
|
||||
'-s',
|
||||
'sky:V',
|
||||
'chromium:D',
|
||||
'ActivityManager:W',
|
||||
'*:F',
|
||||
], prefix: 'android: ');
|
||||
}
|
||||
|
||||
void startTracing(AndroidApk apk) {
|
||||
runCheckedSync([
|
||||
adbPath,
|
||||
'shell',
|
||||
'am',
|
||||
'broadcast',
|
||||
'-a',
|
||||
'${apk.id}.TRACING_START'
|
||||
]);
|
||||
}
|
||||
|
||||
String stopTracing(AndroidApk apk) {
|
||||
clearLogs();
|
||||
runCheckedSync([
|
||||
adbPath,
|
||||
'shell',
|
||||
'am',
|
||||
'broadcast',
|
||||
'-a',
|
||||
'${apk.id}.TRACING_STOP'
|
||||
]);
|
||||
|
||||
RegExp traceRegExp = new RegExp(r'Saving trace to (\S+)', multiLine: true);
|
||||
RegExp completeRegExp = new RegExp(r'Trace complete', multiLine: true);
|
||||
|
||||
String tracePath = null;
|
||||
bool isComplete = false;
|
||||
while (!isComplete) {
|
||||
String logs = runSync([adbPath, 'logcat', '-d']);
|
||||
Match fileMatch = traceRegExp.firstMatch(logs);
|
||||
if (fileMatch[1] != null) {
|
||||
tracePath = fileMatch[1];
|
||||
}
|
||||
isComplete = completeRegExp.hasMatch(logs);
|
||||
}
|
||||
|
||||
if (tracePath != null) {
|
||||
runSync([adbPath, 'shell', 'run-as', apk.id, 'chmod', '777', tracePath]);
|
||||
runSync([adbPath, 'pull', tracePath]);
|
||||
runSync([adbPath, 'shell', 'rm', tracePath]);
|
||||
return path.basename(tracePath);
|
||||
}
|
||||
_logging.warning('No trace file detected. '
|
||||
'Did you remember to start the trace before stopping it?');
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
bool isConnected() => _hasValidAndroid;
|
||||
}
|
||||
|
||||
class DeviceStore {
|
||||
final AndroidDevice android;
|
||||
final IOSDevice iOS;
|
||||
final IOSSimulator iOSSimulator;
|
||||
|
||||
List<Device> get all {
|
||||
List<Device> result = <Device>[];
|
||||
if (android != null)
|
||||
result.add(android);
|
||||
if (iOS != null)
|
||||
result.add(iOS);
|
||||
if (iOSSimulator != null)
|
||||
result.add(iOSSimulator);
|
||||
return result;
|
||||
}
|
||||
|
||||
DeviceStore({
|
||||
this.android,
|
||||
this.iOS,
|
||||
this.iOSSimulator
|
||||
});
|
||||
|
||||
factory DeviceStore.forConfigs(List<BuildConfiguration> configs) {
|
||||
AndroidDevice android;
|
||||
IOSDevice iOS;
|
||||
IOSSimulator iOSSimulator;
|
||||
|
||||
for (BuildConfiguration config in configs) {
|
||||
switch (config.targetPlatform) {
|
||||
case TargetPlatform.android:
|
||||
assert(android == null);
|
||||
android = new AndroidDevice();
|
||||
break;
|
||||
case TargetPlatform.iOS:
|
||||
assert(iOS == null);
|
||||
iOS = new IOSDevice();
|
||||
break;
|
||||
case TargetPlatform.iOSSimulator:
|
||||
assert(iOSSimulator == null);
|
||||
iOSSimulator = new IOSSimulator();
|
||||
break;
|
||||
case TargetPlatform.linux:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return new DeviceStore(android: android, iOS: iOS, iOSSimulator: iOSSimulator);
|
||||
}
|
||||
}
|
91
packages/flutter_tools/lib/src/os_utils.dart
Normal file
91
packages/flutter_tools/lib/src/os_utils.dart
Normal file
@ -0,0 +1,91 @@
|
||||
// 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:io';
|
||||
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
import 'process.dart';
|
||||
|
||||
final OperatingSystemUtils osUtils = new OperatingSystemUtils._();
|
||||
|
||||
final Logger _logging = new Logger('sky_tools.os');
|
||||
|
||||
abstract class OperatingSystemUtils {
|
||||
factory OperatingSystemUtils._() {
|
||||
if (Platform.isWindows) {
|
||||
return new _WindowsUtils();
|
||||
} else if (Platform.isMacOS) {
|
||||
return new _MacUtils();
|
||||
} else {
|
||||
return new _LinuxUtils();
|
||||
}
|
||||
}
|
||||
|
||||
/// Make the given file executable. This may be a no-op on some platforms.
|
||||
ProcessResult makeExecutable(File file);
|
||||
|
||||
/// A best-effort attempt to kill all listeners on the given TCP port.
|
||||
void killTcpPortListeners(int tcpPort);
|
||||
}
|
||||
|
||||
abstract class _PosixUtils implements OperatingSystemUtils {
|
||||
ProcessResult makeExecutable(File file) {
|
||||
return Process.runSync('chmod', ['u+x', file.path]);
|
||||
}
|
||||
}
|
||||
|
||||
class _WindowsUtils implements OperatingSystemUtils {
|
||||
// This is a no-op.
|
||||
ProcessResult makeExecutable(File file) {
|
||||
return new ProcessResult(0, 0, null, null);
|
||||
}
|
||||
|
||||
void killTcpPortListeners(int tcpPort) {
|
||||
// Get list of network processes and split on newline
|
||||
List<String> processes = runSync(['netstat.exe','-ano']).split("\r");
|
||||
|
||||
// List entries from netstat is formatted like so:
|
||||
// TCP 192.168.2.11:50945 192.30.252.90:443 LISTENING 1304
|
||||
// This regexp is to find process where the the port exactly matches
|
||||
RegExp pattern = new RegExp(':$tcpPort[ ]+');
|
||||
|
||||
// Split the columns by 1 or more spaces
|
||||
RegExp columnPattern = new RegExp('[ ]+');
|
||||
processes.forEach((String process) {
|
||||
if (process.contains(pattern)) {
|
||||
// The last column is the Process ID
|
||||
String processId = process.split(columnPattern).last;
|
||||
// Force and Tree kill the process
|
||||
_logging.info('kill $processId');
|
||||
runSync(['TaskKill.exe', '/F', '/T', '/PID', processId]);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _MacUtils extends _PosixUtils {
|
||||
void killTcpPortListeners(int tcpPort) {
|
||||
String pids = runSync(['lsof', '-i', ':$tcpPort', '-t']).trim();
|
||||
if (pids.isNotEmpty) {
|
||||
// Handle multiple returned pids.
|
||||
for (String pidString in pids.split('\n')) {
|
||||
// Killing a pid with a shell command from within dart is hard, so use a
|
||||
// library command, but it's still nice to give the equivalent command
|
||||
// when doing verbose logging.
|
||||
_logging.info('kill $pidString');
|
||||
|
||||
int pid = int.parse(pidString, onError: (_) => null);
|
||||
if (pid != null)
|
||||
Process.killPid(pid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _LinuxUtils extends _PosixUtils {
|
||||
void killTcpPortListeners(int tcpPort) {
|
||||
runSync(['fuser', '-k', '$tcpPort/tcp']);
|
||||
}
|
||||
}
|
96
packages/flutter_tools/lib/src/process.dart
Normal file
96
packages/flutter_tools/lib/src/process.dart
Normal file
@ -0,0 +1,96 @@
|
||||
// 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:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
final Logger _logging = new Logger('sky_tools.process');
|
||||
|
||||
/// This runs the command and streams stdout/stderr from the child process to
|
||||
/// this process' stdout/stderr.
|
||||
Future<int> runCommandAndStreamOutput(List<String> cmd,
|
||||
{String prefix: '', RegExp filter}) async {
|
||||
_logging.info(cmd.join(' '));
|
||||
Process proc =
|
||||
await Process.start(cmd[0], cmd.getRange(1, cmd.length).toList());
|
||||
proc.stdout.transform(UTF8.decoder).listen((String data) {
|
||||
List<String> dataLines = data.trimRight().split('\n');
|
||||
if (filter != null) {
|
||||
dataLines = dataLines.where((String s) => filter.hasMatch(s)).toList();
|
||||
}
|
||||
if (dataLines.length > 0) {
|
||||
stdout.write('$prefix${dataLines.join('\n$prefix')}\n');
|
||||
}
|
||||
});
|
||||
proc.stderr.transform(UTF8.decoder).listen((String data) {
|
||||
List<String> dataLines = data.trimRight().split('\n');
|
||||
if (filter != null) {
|
||||
dataLines = dataLines.where((String s) => filter.hasMatch(s));
|
||||
}
|
||||
if (dataLines.length > 0) {
|
||||
stderr.write('$prefix${dataLines.join('\n$prefix')}\n');
|
||||
}
|
||||
});
|
||||
return proc.exitCode;
|
||||
}
|
||||
|
||||
Future runAndKill(List<String> cmd, Duration timeout) async {
|
||||
Future<Process> proc = runDetached(cmd);
|
||||
return new Future.delayed(timeout, () async {
|
||||
_logging.info('Intentionally killing ${cmd[0]}');
|
||||
Process.killPid((await proc).pid);
|
||||
});
|
||||
}
|
||||
|
||||
Future<Process> runDetached(List<String> cmd) async {
|
||||
_logging.info(cmd.join(' '));
|
||||
Future<Process> proc = Process.start(
|
||||
cmd[0], cmd.getRange(1, cmd.length).toList(),
|
||||
mode: ProcessStartMode.DETACHED);
|
||||
return proc;
|
||||
}
|
||||
|
||||
/// Run cmd and return stdout.
|
||||
/// Throws an error if cmd exits with a non-zero value.
|
||||
String runCheckedSync(List<String> cmd) =>
|
||||
_runWithLoggingSync(cmd, checked: true);
|
||||
|
||||
/// Run cmd and return stdout.
|
||||
String runSync(List<String> cmd) => _runWithLoggingSync(cmd);
|
||||
|
||||
/// Return the platform specific name for the given Dart SDK binary. So, `pub`
|
||||
/// ==> `pub.bat`.
|
||||
String sdkBinaryName(String name) {
|
||||
return Platform.isWindows ? '${name}.bat' : name;
|
||||
}
|
||||
|
||||
String _runWithLoggingSync(List<String> cmd, {bool checked: false}) {
|
||||
_logging.info(cmd.join(' '));
|
||||
ProcessResult results =
|
||||
Process.runSync(cmd[0], cmd.getRange(1, cmd.length).toList());
|
||||
if (results.exitCode != 0) {
|
||||
String errorDescription = 'Error code ${results.exitCode} '
|
||||
'returned when attempting to run command: ${cmd.join(' ')}';
|
||||
_logging.fine(errorDescription);
|
||||
if (results.stderr.length > 0) {
|
||||
_logging.info('Errors logged: ${results.stderr.trim()}');
|
||||
}
|
||||
|
||||
if (checked) {
|
||||
throw errorDescription;
|
||||
}
|
||||
}
|
||||
_logging.fine(results.stdout.trim());
|
||||
return results.stdout;
|
||||
}
|
||||
|
||||
class ProcessExit implements Exception {
|
||||
final int exitCode;
|
||||
ProcessExit(this.exitCode);
|
||||
String get message => 'ProcessExit: ${exitCode}';
|
||||
String toString() => message;
|
||||
}
|
20
packages/flutter_tools/lib/src/test/json_socket.dart
Normal file
20
packages/flutter_tools/lib/src/test/json_socket.dart
Normal file
@ -0,0 +1,20 @@
|
||||
// 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:convert';
|
||||
import 'dart:io';
|
||||
|
||||
class JSONSocket {
|
||||
JSONSocket(WebSocket socket, this.unusualTermination)
|
||||
: _socket = socket, stream = socket.map(JSON.decode).asBroadcastStream();
|
||||
|
||||
final WebSocket _socket;
|
||||
final Stream stream;
|
||||
final Future<String> unusualTermination;
|
||||
|
||||
void send(dynamic data) {
|
||||
_socket.add(JSON.encode(data));
|
||||
}
|
||||
}
|
189
packages/flutter_tools/lib/src/test/loader.dart
Normal file
189
packages/flutter_tools/lib/src/test/loader.dart
Normal file
@ -0,0 +1,189 @@
|
||||
// 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:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:sky_tools/src/test/json_socket.dart';
|
||||
import 'package:sky_tools/src/test/remote_test.dart';
|
||||
import 'package:stack_trace/stack_trace.dart';
|
||||
import 'package:test/src/backend/group.dart';
|
||||
import 'package:test/src/backend/metadata.dart';
|
||||
import 'package:test/src/backend/test_platform.dart';
|
||||
import 'package:test/src/runner/configuration.dart';
|
||||
import 'package:test/src/runner/hack_load_vm_file_hook.dart' as hack;
|
||||
import 'package:test/src/runner/load_exception.dart';
|
||||
import 'package:test/src/runner/runner_suite.dart';
|
||||
import 'package:test/src/runner/vm/environment.dart';
|
||||
import 'package:test/src/util/io.dart';
|
||||
import 'package:test/src/util/remote_exception.dart';
|
||||
|
||||
void installHook() {
|
||||
hack.loadVMFileHook = _loadVMFile;
|
||||
}
|
||||
|
||||
final String _kSkyShell = Platform.environment['SKY_SHELL'];
|
||||
const String _kHost = '127.0.0.1';
|
||||
const String _kPath = '/runner';
|
||||
|
||||
// Right now a bunch of our tests crash or assert after the tests have finished running.
|
||||
// Mostly this is just because the test puts the framework in an inconsistent state with
|
||||
// a scheduled microtask that verifies that state. Eventually we should fix all these
|
||||
// problems but for now we'll just paper over them.
|
||||
const bool kExpectAllTestsToCloseCleanly = false;
|
||||
|
||||
class _ServerInfo {
|
||||
final String url;
|
||||
final Future<WebSocket> socket;
|
||||
final HttpServer server;
|
||||
|
||||
_ServerInfo(this.server, this.url, this.socket);
|
||||
}
|
||||
|
||||
Future<_ServerInfo> _createServer() async {
|
||||
HttpServer server = await HttpServer.bind(_kHost, 0);
|
||||
Completer<WebSocket> socket = new Completer<WebSocket>();
|
||||
server.listen((HttpRequest request) {
|
||||
if (request.uri.path == _kPath)
|
||||
socket.complete(WebSocketTransformer.upgrade(request));
|
||||
});
|
||||
return new _ServerInfo(server, 'ws://$_kHost:${server.port}$_kPath', socket.future);
|
||||
}
|
||||
|
||||
Future<Process> _startProcess(String path, { String packageRoot }) {
|
||||
assert(_kSkyShell != null); // Please provide the path to the shell in the SKY_SHELL environment variable.
|
||||
return Process.start(_kSkyShell, [
|
||||
'--enable-checked-mode',
|
||||
'--non-interactive',
|
||||
'--package-root=$packageRoot',
|
||||
path,
|
||||
]);
|
||||
}
|
||||
|
||||
Future<RunnerSuite> _loadVMFile(String path,
|
||||
Metadata metadata,
|
||||
Configuration config) async {
|
||||
String encodedMetadata = Uri.encodeComponent(JSON.encode(
|
||||
metadata.serialize()));
|
||||
_ServerInfo info = await _createServer();
|
||||
Directory tempDir = await Directory.systemTemp.createTemp(
|
||||
'dart_test_listener');
|
||||
File listenerFile = new File('${tempDir.path}/listener.dart');
|
||||
await listenerFile.create();
|
||||
await listenerFile.writeAsString('''
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:test/src/backend/metadata.dart';
|
||||
import 'package:sky_tools/src/test/remote_listener.dart';
|
||||
|
||||
import '${p.toUri(p.absolute(path))}' as test;
|
||||
|
||||
void main() {
|
||||
String server = Uri.decodeComponent('${Uri.encodeComponent(info.url)}');
|
||||
Metadata metadata = new Metadata.deserialize(
|
||||
JSON.decode(Uri.decodeComponent('$encodedMetadata')));
|
||||
RemoteListener.start(server, metadata, () => test.main);
|
||||
}
|
||||
''');
|
||||
|
||||
Completer<Iterable<RemoteTest>> completer = new Completer<Iterable<RemoteTest>>();
|
||||
Completer<String> deathCompleter = new Completer();
|
||||
|
||||
Process process = await _startProcess(
|
||||
listenerFile.path,
|
||||
packageRoot: p.absolute(config.packageRoot)
|
||||
);
|
||||
|
||||
Future cleanupTempDirectory() async {
|
||||
if (tempDir == null)
|
||||
return;
|
||||
Directory dirToDelete = tempDir;
|
||||
tempDir = null;
|
||||
await dirToDelete.delete(recursive: true);
|
||||
}
|
||||
|
||||
process.exitCode.then((int exitCode) async {
|
||||
try {
|
||||
info.server.close(force: true);
|
||||
await cleanupTempDirectory();
|
||||
String output = '';
|
||||
if (exitCode < 0) {
|
||||
// Abnormal termination (high bit of signed 8-bit exitCode is set)
|
||||
switch (exitCode) {
|
||||
case -0x0f: // ProcessSignal.SIGTERM
|
||||
break; // we probably killed it ourselves
|
||||
case -0x0b: // ProcessSignal.SIGSEGV
|
||||
output += 'Segmentation fault in subprocess for: $path\n';
|
||||
break;
|
||||
default:
|
||||
output += 'Unexpected exit code $exitCode from subprocess for: $path\n';
|
||||
}
|
||||
}
|
||||
String stdout = await process.stdout.transform(UTF8.decoder).join('\n');
|
||||
String stderr = await process.stderr.transform(UTF8.decoder).join('\n');
|
||||
if (stdout != '')
|
||||
output += '\nstdout:\n$stdout';
|
||||
if (stderr != '')
|
||||
output += '\nstderr:\n$stderr';
|
||||
if (!completer.isCompleted) {
|
||||
if (output == '')
|
||||
output = 'No output.';
|
||||
completer.completeError(
|
||||
new LoadException(path, output),
|
||||
new Trace.current()
|
||||
);
|
||||
} else {
|
||||
if (kExpectAllTestsToCloseCleanly && output != '')
|
||||
print('Unexpected failure after test claimed to pass:\n$output');
|
||||
}
|
||||
deathCompleter.complete(output);
|
||||
} catch (e) {
|
||||
// Throwing inside this block causes all kinds of hard-to-debug issues
|
||||
// like stack overflows and hangs. So catch everything just in case.
|
||||
print("exception while handling subprocess termination: $e");
|
||||
}
|
||||
});
|
||||
|
||||
JSONSocket socket = new JSONSocket(await info.socket, deathCompleter.future);
|
||||
|
||||
await cleanupTempDirectory();
|
||||
|
||||
StreamSubscription subscription;
|
||||
subscription = socket.stream.listen((response) {
|
||||
if (response["type"] == "print") {
|
||||
print(response["line"]);
|
||||
} else if (response["type"] == "loadException") {
|
||||
process.kill(ProcessSignal.SIGTERM);
|
||||
completer.completeError(
|
||||
new LoadException(path, response["message"]),
|
||||
new Trace.current());
|
||||
} else if (response["type"] == "error") {
|
||||
process.kill(ProcessSignal.SIGTERM);
|
||||
AsyncError asyncError = RemoteException.deserialize(response["error"]);
|
||||
completer.completeError(
|
||||
new LoadException(path, asyncError.error),
|
||||
asyncError.stackTrace);
|
||||
} else {
|
||||
assert(response["type"] == "success");
|
||||
subscription.cancel();
|
||||
completer.complete(response["tests"].map((test) {
|
||||
var testMetadata = new Metadata.deserialize(test['metadata']);
|
||||
return new RemoteTest(test['name'], testMetadata, socket, test['index']);
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
Iterable<RemoteTest> entries = await completer.future;
|
||||
|
||||
return new RunnerSuite(
|
||||
const VMEnvironment(),
|
||||
new Group.root(entries, metadata: metadata),
|
||||
path: path,
|
||||
platform: TestPlatform.vm,
|
||||
os: currentOS,
|
||||
onClose: () { process.kill(ProcessSignal.SIGTERM); }
|
||||
);
|
||||
}
|
164
packages/flutter_tools/lib/src/test/remote_listener.dart
Normal file
164
packages/flutter_tools/lib/src/test/remote_listener.dart
Normal file
@ -0,0 +1,164 @@
|
||||
// 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:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
|
||||
import 'package:stack_trace/stack_trace.dart';
|
||||
import 'package:test/src/backend/declarer.dart';
|
||||
import 'package:test/src/backend/live_test.dart';
|
||||
import 'package:test/src/backend/metadata.dart';
|
||||
import 'package:test/src/backend/operating_system.dart';
|
||||
import 'package:test/src/backend/suite.dart';
|
||||
import 'package:test/src/backend/test_platform.dart';
|
||||
import 'package:test/src/backend/test.dart';
|
||||
import 'package:test/src/util/remote_exception.dart';
|
||||
|
||||
final OperatingSystem currentOS = (() {
|
||||
var name = Platform.operatingSystem;
|
||||
var os = OperatingSystem.findByIoName(name);
|
||||
if (os != null) return os;
|
||||
|
||||
throw new UnsupportedError('Unsupported operating system "$name".');
|
||||
})();
|
||||
|
||||
typedef AsyncFunction();
|
||||
|
||||
class RemoteListener {
|
||||
RemoteListener._(this._suite, this._socket);
|
||||
|
||||
final Suite _suite;
|
||||
final WebSocket _socket;
|
||||
final Set<LiveTest> _liveTests = new Set<LiveTest>();
|
||||
|
||||
static Future start(String server, Metadata metadata, Function getMain()) async {
|
||||
WebSocket socket = await WebSocket.connect(server);
|
||||
// Capture any top-level errors (mostly lazy syntax errors, since other are
|
||||
// caught below) and report them to the parent isolate. We set errors
|
||||
// non-fatal because otherwise they'll be double-printed.
|
||||
var errorPort = new ReceivePort();
|
||||
Isolate.current.setErrorsFatal(false);
|
||||
Isolate.current.addErrorListener(errorPort.sendPort);
|
||||
errorPort.listen((message) {
|
||||
// Masquerade as an IsolateSpawnException because that's what this would
|
||||
// be if the error had been detected statically.
|
||||
var error = new IsolateSpawnException(message[0]);
|
||||
var stackTrace =
|
||||
message[1] == null ? new Trace([]) : new Trace.parse(message[1]);
|
||||
socket.add(JSON.encode({
|
||||
"type": "error",
|
||||
"error": RemoteException.serialize(error, stackTrace)
|
||||
}));
|
||||
});
|
||||
|
||||
var main;
|
||||
try {
|
||||
main = getMain();
|
||||
} on NoSuchMethodError catch (_) {
|
||||
_sendLoadException(socket, "No top-level main() function defined.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (main is! Function) {
|
||||
_sendLoadException(socket, "Top-level main getter is not a function.");
|
||||
return;
|
||||
} else if (main is! AsyncFunction) {
|
||||
_sendLoadException(
|
||||
socket, "Top-level main() function takes arguments.");
|
||||
return;
|
||||
}
|
||||
|
||||
Declarer declarer = new Declarer(metadata);
|
||||
try {
|
||||
await runZoned(() => new Future.sync(main), zoneValues: {
|
||||
#test.declarer: declarer
|
||||
}, zoneSpecification: new ZoneSpecification(print: (_, __, ___, line) {
|
||||
socket.add(JSON.encode({"type": "print", "line": line}));
|
||||
}));
|
||||
} catch (error, stackTrace) {
|
||||
socket.add(JSON.encode({
|
||||
"type": "error",
|
||||
"error": RemoteException.serialize(error, stackTrace)
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
Suite suite = new Suite(declarer.build(),
|
||||
platform: TestPlatform.vm, os: currentOS);
|
||||
new RemoteListener._(suite, socket)._listen();
|
||||
}
|
||||
|
||||
static void _sendLoadException(WebSocket socket, String message) {
|
||||
socket.add(JSON.encode({"type": "loadException", "message": message}));
|
||||
}
|
||||
|
||||
void _send(data) {
|
||||
_socket.add(JSON.encode(data));
|
||||
}
|
||||
|
||||
void _listen() {
|
||||
List tests = [];
|
||||
for (var i = 0; i < _suite.group.entries.length; i++) {
|
||||
// TODO(ianh): entries[] might return a Group instead of a Test. We don't
|
||||
// currently support nested groups.
|
||||
Test test = _suite.group.entries[i];
|
||||
tests.add({
|
||||
"name": test.name,
|
||||
"metadata": test.metadata.serialize(),
|
||||
"index": i,
|
||||
});
|
||||
}
|
||||
|
||||
_send({"type": "success", "tests": tests});
|
||||
_socket.listen(_handleCommand);
|
||||
}
|
||||
|
||||
void _handleCommand(String data) {
|
||||
var message = JSON.decode(data);
|
||||
if (message['command'] == 'run') {
|
||||
// TODO(ianh): entries[] might return a Group instead of a Test. We don't
|
||||
// currently support nested groups.
|
||||
Test test = _suite.group.entries[message['index']];
|
||||
LiveTest liveTest = test.load(_suite);
|
||||
_liveTests.add(liveTest);
|
||||
|
||||
liveTest.onStateChange.listen((state) {
|
||||
_send({
|
||||
"type": "state-change",
|
||||
"status": state.status.name,
|
||||
"result": state.result.name
|
||||
});
|
||||
});
|
||||
|
||||
liveTest.onError.listen((asyncError) {
|
||||
_send({
|
||||
"type": "error",
|
||||
"error": RemoteException.serialize(
|
||||
asyncError.error,
|
||||
asyncError.stackTrace
|
||||
)
|
||||
});
|
||||
});
|
||||
|
||||
liveTest.onPrint.listen((line) {
|
||||
_send({"type": "print", "line": line});
|
||||
});
|
||||
|
||||
liveTest.run().then((_) {
|
||||
_send({"type": "complete"});
|
||||
_liveTests.remove(liveTest);
|
||||
});
|
||||
} else if (message['command'] == 'close') {
|
||||
if (_liveTests.isNotEmpty)
|
||||
print('closing with ${_liveTests.length} live tests');
|
||||
for (LiveTest liveTest in _liveTests)
|
||||
liveTest.close();
|
||||
_liveTests.clear();
|
||||
} else {
|
||||
print('remote_listener.dart: ignoring command "${message["command"]}" from test harness');
|
||||
}
|
||||
}
|
||||
}
|
95
packages/flutter_tools/lib/src/test/remote_test.dart
Normal file
95
packages/flutter_tools/lib/src/test/remote_test.dart
Normal file
@ -0,0 +1,95 @@
|
||||
// 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 'package:stack_trace/stack_trace.dart';
|
||||
import 'package:test/src/backend/live_test.dart';
|
||||
import 'package:test/src/backend/live_test_controller.dart';
|
||||
import 'package:test/src/backend/metadata.dart';
|
||||
import 'package:test/src/backend/operating_system.dart';
|
||||
import 'package:test/src/backend/state.dart';
|
||||
import 'package:test/src/backend/suite.dart';
|
||||
import 'package:test/src/backend/test.dart';
|
||||
import 'package:test/src/backend/test_platform.dart';
|
||||
import 'package:test/src/util/remote_exception.dart';
|
||||
|
||||
import 'package:sky_tools/src/test/json_socket.dart';
|
||||
|
||||
class RemoteTest extends Test {
|
||||
RemoteTest(this.name, this.metadata, this._socket, this._index);
|
||||
|
||||
final String name;
|
||||
final Metadata metadata;
|
||||
final JSONSocket _socket;
|
||||
final int _index;
|
||||
|
||||
LiveTest load(Suite suite) {
|
||||
LiveTestController controller;
|
||||
StreamSubscription subscription;
|
||||
|
||||
controller = new LiveTestController(suite, this, () async {
|
||||
|
||||
controller.setState(const State(Status.running, Result.success));
|
||||
_socket.send({'command': 'run', 'index': _index});
|
||||
|
||||
subscription = _socket.stream.listen((message) {
|
||||
if (message['type'] == 'error') {
|
||||
AsyncError asyncError = RemoteException.deserialize(message['error']);
|
||||
controller.addError(asyncError.error, asyncError.stackTrace);
|
||||
} else if (message['type'] == 'state-change') {
|
||||
controller.setState(
|
||||
new State(
|
||||
new Status.parse(message['status']),
|
||||
new Result.parse(message['result'])));
|
||||
} else if (message['type'] == 'print') {
|
||||
controller.print(message['line']);
|
||||
} else {
|
||||
assert(message['type'] == 'complete');
|
||||
subscription.cancel();
|
||||
subscription = null;
|
||||
controller.completer.complete();
|
||||
}
|
||||
});
|
||||
|
||||
_socket.unusualTermination.then((String message) {
|
||||
if (subscription != null) {
|
||||
controller.print('Unexpected subprocess termination: $message');
|
||||
controller.addError(new Exception('Unexpected subprocess termination.'), new Trace.current());
|
||||
controller.setState(new State(Status.complete, Result.error));
|
||||
subscription.cancel();
|
||||
subscription = null;
|
||||
controller.completer.complete();
|
||||
}
|
||||
});
|
||||
|
||||
}, () async {
|
||||
_socket.send({'command': 'close'});
|
||||
if (subscription != null) {
|
||||
subscription.cancel();
|
||||
subscription = null;
|
||||
}
|
||||
});
|
||||
return controller.liveTest;
|
||||
}
|
||||
|
||||
Test change({String name, Metadata metadata}) {
|
||||
if (name == name && metadata == this.metadata) return this;
|
||||
if (name == null) name = this.name;
|
||||
if (metadata == null) metadata = this.metadata;
|
||||
return new RemoteTest(name, metadata, _socket, _index);
|
||||
}
|
||||
|
||||
// TODO(ianh): Implement this if we need it.
|
||||
Test forPlatform(TestPlatform platform, {OperatingSystem os}) {
|
||||
if (!metadata.testOn.evaluate(platform, os: os))
|
||||
return null;
|
||||
return new RemoteTest(
|
||||
name,
|
||||
metadata.forPlatform(platform, os: os),
|
||||
_socket,
|
||||
_index
|
||||
);
|
||||
}
|
||||
}
|
49
packages/flutter_tools/lib/src/toolchain.dart
Normal file
49
packages/flutter_tools/lib/src/toolchain.dart
Normal file
@ -0,0 +1,49 @@
|
||||
// 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 'package:path/path.dart' as path;
|
||||
|
||||
import 'artifacts.dart';
|
||||
import 'build_configuration.dart';
|
||||
import 'process.dart';
|
||||
|
||||
class Compiler {
|
||||
Compiler(this._path);
|
||||
|
||||
String _path;
|
||||
|
||||
Future<int> compile({
|
||||
String mainPath,
|
||||
String snapshotPath
|
||||
}) {
|
||||
return runCommandAndStreamOutput([
|
||||
_path,
|
||||
mainPath,
|
||||
'--package-root=${ArtifactStore.packageRoot}',
|
||||
'--snapshot=$snapshotPath'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> _getCompilerPath(BuildConfiguration config) async {
|
||||
if (config.type != BuildType.prebuilt)
|
||||
return path.join(config.buildDir, 'clang_x64', 'sky_snapshot');
|
||||
Artifact artifact = ArtifactStore.getArtifact(
|
||||
type: ArtifactType.snapshot, hostPlatform: config.hostPlatform);
|
||||
return await ArtifactStore.getPath(artifact);
|
||||
}
|
||||
|
||||
class Toolchain {
|
||||
Toolchain({ this.compiler });
|
||||
|
||||
final Compiler compiler;
|
||||
|
||||
static Future<Toolchain> forConfigs(List<BuildConfiguration> configs) async {
|
||||
// TODO(abarth): Shouldn't we consider all the configs?
|
||||
String compilerPath = await _getCompilerPath(configs.first);
|
||||
return new Toolchain(compiler: new Compiler(compilerPath));
|
||||
}
|
||||
}
|
38
packages/flutter_tools/pubspec.yaml
Normal file
38
packages/flutter_tools/pubspec.yaml
Normal file
@ -0,0 +1,38 @@
|
||||
name: sky_tools
|
||||
version: 0.0.37
|
||||
description: Tools for building Flutter applications
|
||||
homepage: http://flutter.io
|
||||
author: Flutter Authors <flutter-dev@googlegroups.com>
|
||||
|
||||
environment:
|
||||
sdk: '>=1.12.0 <2.0.0'
|
||||
|
||||
dependencies:
|
||||
analyzer: ">=0.26.1+17" # see note below
|
||||
archive: ^1.0.20
|
||||
args: ^0.13.0
|
||||
flx: ">=0.0.7 <0.1.0"
|
||||
crypto: ^0.9.1
|
||||
mustache4dart: ^1.0.0
|
||||
path: ^1.3.0
|
||||
shelf_route: ^0.13.4
|
||||
shelf_static: ^0.2.3
|
||||
shelf: ^0.6.2
|
||||
stack_trace: ^1.4.0
|
||||
test: ^0.12.5
|
||||
yaml: ^2.1.3
|
||||
|
||||
# A note about 'analyzer':
|
||||
# We don't actually depend on 'analyzer', but 'test' does. We aren't
|
||||
# compatible with some older versions of 'analyzer'. We lie here,
|
||||
# saying we do depend on it, so that we constrain the version that
|
||||
# 'test' will get to a version that we'll probably be ok with. (This
|
||||
# is why there's no upper bound on our dependency.)
|
||||
# See also https://github.com/dart-lang/pub/issues/1356
|
||||
|
||||
dev_dependencies:
|
||||
mockito: "^0.10.1"
|
||||
|
||||
# Add the bin/sky_tools.dart script to the scripts pub installs.
|
||||
executables:
|
||||
sky_tools:
|
29
packages/flutter_tools/test/all.dart
Normal file
29
packages/flutter_tools/test/all.dart
Normal file
@ -0,0 +1,29 @@
|
||||
// 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 'android_device_test.dart' as android_device_test;
|
||||
import 'daemon_test.dart' as daemon_test;
|
||||
import 'init_test.dart' as init_test;
|
||||
import 'install_test.dart' as install_test;
|
||||
import 'listen_test.dart' as listen_test;
|
||||
import 'list_test.dart' as list_test;
|
||||
import 'logs_test.dart' as logs_test;
|
||||
import 'os_utils_test.dart' as os_utils_test;
|
||||
import 'start_test.dart' as start_test;
|
||||
import 'stop_test.dart' as stop_test;
|
||||
import 'trace_test.dart' as trace_test;
|
||||
|
||||
main() {
|
||||
android_device_test.defineTests();
|
||||
daemon_test.defineTests();
|
||||
init_test.defineTests();
|
||||
install_test.defineTests();
|
||||
listen_test.defineTests();
|
||||
list_test.defineTests();
|
||||
logs_test.defineTests();
|
||||
os_utils_test.defineTests();
|
||||
start_test.defineTests();
|
||||
stop_test.defineTests();
|
||||
trace_test.defineTests();
|
||||
}
|
30
packages/flutter_tools/test/android_device_test.dart
Normal file
30
packages/flutter_tools/test/android_device_test.dart
Normal file
@ -0,0 +1,30 @@
|
||||
// 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 'package:sky_tools/src/device.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
main() => defineTests();
|
||||
|
||||
defineTests() {
|
||||
group('android_device', () {
|
||||
test('uses the correct default ID', () {
|
||||
AndroidDevice android = new AndroidDevice();
|
||||
expect(android.id, equals(AndroidDevice.defaultDeviceID));
|
||||
});
|
||||
|
||||
test('stores the requested id', () {
|
||||
String deviceId = '1234';
|
||||
AndroidDevice android = new AndroidDevice(id: deviceId);
|
||||
expect(android.id, equals(deviceId));
|
||||
});
|
||||
|
||||
test('correctly creates only one of each requested device id', () {
|
||||
String deviceID = '1234';
|
||||
AndroidDevice a1 = new AndroidDevice(id: deviceID);
|
||||
AndroidDevice a2 = new AndroidDevice(id: deviceID);
|
||||
expect(a1, equals(a2));
|
||||
});
|
||||
});
|
||||
}
|
80
packages/flutter_tools/test/daemon_test.dart
Normal file
80
packages/flutter_tools/test/daemon_test.dart
Normal file
@ -0,0 +1,80 @@
|
||||
// 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 'package:mockito/mockito.dart';
|
||||
import 'package:sky_tools/src/commands/daemon.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import 'src/mocks.dart';
|
||||
|
||||
main() => defineTests();
|
||||
|
||||
defineTests() {
|
||||
group('daemon', () {
|
||||
Daemon daemon;
|
||||
|
||||
tearDown(() {
|
||||
if (daemon != null)
|
||||
return daemon.shutdown();
|
||||
});
|
||||
|
||||
test('daemon.version', () async {
|
||||
StreamController<Map> commands = new StreamController();
|
||||
StreamController<Map> responses = new StreamController();
|
||||
daemon = new Daemon(
|
||||
commands.stream,
|
||||
(Map result) => responses.add(result)
|
||||
);
|
||||
commands.add({'id': 0, 'event': 'daemon.version'});
|
||||
Map response = await responses.stream.first;
|
||||
expect(response['id'], 0);
|
||||
expect(response['result'], isNotEmpty);
|
||||
expect(response['result'] is String, true);
|
||||
});
|
||||
|
||||
test('daemon.shutdown', () async {
|
||||
StreamController<Map> commands = new StreamController();
|
||||
StreamController<Map> responses = new StreamController();
|
||||
daemon = new Daemon(
|
||||
commands.stream,
|
||||
(Map result) => responses.add(result)
|
||||
);
|
||||
commands.add({'id': 0, 'event': 'daemon.shutdown'});
|
||||
return daemon.onExit.then((int code) {
|
||||
expect(code, 0);
|
||||
});
|
||||
});
|
||||
|
||||
test('daemon.stopAll', () async {
|
||||
DaemonCommand command = new DaemonCommand();
|
||||
applyMocksToCommand(command);
|
||||
|
||||
StreamController<Map> commands = new StreamController();
|
||||
StreamController<Map> responses = new StreamController();
|
||||
daemon = new Daemon(
|
||||
commands.stream,
|
||||
(Map result) => responses.add(result),
|
||||
daemonCommand: command
|
||||
);
|
||||
|
||||
MockDeviceStore mockDevices = command.devices;
|
||||
|
||||
when(mockDevices.android.isConnected()).thenReturn(true);
|
||||
when(mockDevices.android.stopApp(any)).thenReturn(true);
|
||||
|
||||
when(mockDevices.iOS.isConnected()).thenReturn(false);
|
||||
when(mockDevices.iOS.stopApp(any)).thenReturn(false);
|
||||
|
||||
when(mockDevices.iOSSimulator.isConnected()).thenReturn(false);
|
||||
when(mockDevices.iOSSimulator.stopApp(any)).thenReturn(false);
|
||||
|
||||
commands.add({'id': 0, 'event': 'app.stopAll'});
|
||||
Map response = await responses.stream.first;
|
||||
expect(response['id'], 0);
|
||||
expect(response['result'], true);
|
||||
});
|
||||
});
|
||||
}
|
53
packages/flutter_tools/test/init_test.dart
Normal file
53
packages/flutter_tools/test/init_test.dart
Normal file
@ -0,0 +1,53 @@
|
||||
// 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:io';
|
||||
|
||||
import 'package:args/command_runner.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:sky_tools/src/commands/init.dart';
|
||||
import 'package:sky_tools/src/process.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
main() => defineTests();
|
||||
|
||||
defineTests() {
|
||||
group('init', () {
|
||||
Directory temp;
|
||||
|
||||
setUp(() {
|
||||
temp = Directory.systemTemp.createTempSync('sky_tools');
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
temp.deleteSync(recursive: true);
|
||||
});
|
||||
|
||||
// This test consistently times out on our windows bot. The code is already
|
||||
// covered on the linux one.
|
||||
if (!Platform.isWindows) {
|
||||
// Verify that we create a project that is well-formed.
|
||||
test('flutter-simple', () async {
|
||||
InitCommand command = new InitCommand();
|
||||
CommandRunner runner = new CommandRunner('test_flutter', '')
|
||||
..addCommand(command);
|
||||
await runner.run(['init', '--out', temp.path])
|
||||
.then((int code) => expect(code, equals(0)));
|
||||
|
||||
String path = p.join(temp.path, 'lib', 'main.dart');
|
||||
expect(new File(path).existsSync(), true);
|
||||
ProcessResult exec = Process.runSync(
|
||||
sdkBinaryName('dartanalyzer'), ['--fatal-warnings', path],
|
||||
workingDirectory: temp.path);
|
||||
if (exec.exitCode != 0) {
|
||||
print(exec.stdout);
|
||||
print(exec.stderr);
|
||||
}
|
||||
expect(exec.exitCode, 0);
|
||||
},
|
||||
// This test can take a while due to network requests.
|
||||
timeout: new Timeout(new Duration(minutes: 2)));
|
||||
}
|
||||
});
|
||||
}
|
61
packages/flutter_tools/test/install_test.dart
Normal file
61
packages/flutter_tools/test/install_test.dart
Normal file
@ -0,0 +1,61 @@
|
||||
// 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 'package:args/command_runner.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
import 'package:sky_tools/src/commands/install.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import 'src/mocks.dart';
|
||||
|
||||
main() => defineTests();
|
||||
|
||||
defineTests() {
|
||||
group('install', () {
|
||||
test('returns 0 when Android is connected and ready for an install', () {
|
||||
InstallCommand command = new InstallCommand();
|
||||
applyMocksToCommand(command);
|
||||
MockDeviceStore mockDevices = command.devices;
|
||||
|
||||
when(mockDevices.android.isConnected()).thenReturn(true);
|
||||
when(mockDevices.android.isAppInstalled(any)).thenReturn(false);
|
||||
when(mockDevices.android.installApp(any)).thenReturn(true);
|
||||
|
||||
when(mockDevices.iOS.isConnected()).thenReturn(false);
|
||||
when(mockDevices.iOS.isAppInstalled(any)).thenReturn(false);
|
||||
when(mockDevices.iOS.installApp(any)).thenReturn(false);
|
||||
|
||||
when(mockDevices.iOSSimulator.isConnected()).thenReturn(false);
|
||||
when(mockDevices.iOSSimulator.isAppInstalled(any)).thenReturn(false);
|
||||
when(mockDevices.iOSSimulator.installApp(any)).thenReturn(false);
|
||||
|
||||
|
||||
CommandRunner runner = new CommandRunner('test_flutter', '')
|
||||
..addCommand(command);
|
||||
runner.run(['install']).then((int code) => expect(code, equals(0)));
|
||||
});
|
||||
|
||||
test('returns 0 when iOS is connected and ready for an install', () {
|
||||
InstallCommand command = new InstallCommand();
|
||||
applyMocksToCommand(command);
|
||||
MockDeviceStore mockDevices = command.devices;
|
||||
|
||||
when(mockDevices.android.isConnected()).thenReturn(false);
|
||||
when(mockDevices.android.isAppInstalled(any)).thenReturn(false);
|
||||
when(mockDevices.android.installApp(any)).thenReturn(false);
|
||||
|
||||
when(mockDevices.iOS.isConnected()).thenReturn(true);
|
||||
when(mockDevices.iOS.isAppInstalled(any)).thenReturn(false);
|
||||
when(mockDevices.iOS.installApp(any)).thenReturn(true);
|
||||
|
||||
when(mockDevices.iOSSimulator.isConnected()).thenReturn(false);
|
||||
when(mockDevices.iOSSimulator.isAppInstalled(any)).thenReturn(false);
|
||||
when(mockDevices.iOSSimulator.installApp(any)).thenReturn(false);
|
||||
|
||||
CommandRunner runner = new CommandRunner('test_flutter', '')
|
||||
..addCommand(command);
|
||||
runner.run(['install']).then((int code) => expect(code, equals(0)));
|
||||
});
|
||||
});
|
||||
}
|
44
packages/flutter_tools/test/list_test.dart
Normal file
44
packages/flutter_tools/test/list_test.dart
Normal file
@ -0,0 +1,44 @@
|
||||
// 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:io';
|
||||
|
||||
import 'package:args/command_runner.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
import 'package:sky_tools/src/commands/list.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import 'src/mocks.dart';
|
||||
|
||||
main() => defineTests();
|
||||
|
||||
defineTests() {
|
||||
group('list', () {
|
||||
test('returns 0 when called', () {
|
||||
final String mockCommand = Platform.isWindows ? 'cmd /c echo' : 'echo';
|
||||
|
||||
ListCommand command = new ListCommand();
|
||||
applyMocksToCommand(command);
|
||||
MockDeviceStore mockDevices = command.devices;
|
||||
|
||||
// Avoid relying on adb being installed on the test system.
|
||||
// Instead, cause the test to run the echo command.
|
||||
when(mockDevices.android.adbPath).thenReturn(mockCommand);
|
||||
|
||||
// Avoid relying on idevice* being installed on the test system.
|
||||
// Instead, cause the test to run the echo command.
|
||||
when(mockDevices.iOS.informerPath).thenReturn(mockCommand);
|
||||
when(mockDevices.iOS.installerPath).thenReturn(mockCommand);
|
||||
when(mockDevices.iOS.listerPath).thenReturn(mockCommand);
|
||||
|
||||
// Avoid relying on xcrun being installed on the test system.
|
||||
// Instead, cause the test to run the echo command.
|
||||
when(mockDevices.iOSSimulator.xcrunPath).thenReturn(mockCommand);
|
||||
|
||||
CommandRunner runner = new CommandRunner('test_flutter', '')
|
||||
..addCommand(command);
|
||||
runner.run(['list']).then((int code) => expect(code, equals(0)));
|
||||
});
|
||||
});
|
||||
}
|
30
packages/flutter_tools/test/listen_test.dart
Normal file
30
packages/flutter_tools/test/listen_test.dart
Normal file
@ -0,0 +1,30 @@
|
||||
// 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 'package:args/command_runner.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
import 'package:sky_tools/src/commands/listen.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import 'src/mocks.dart';
|
||||
|
||||
main() => defineTests();
|
||||
|
||||
defineTests() {
|
||||
group('listen', () {
|
||||
test('returns 0 when no device is connected', () {
|
||||
ListenCommand command = new ListenCommand(singleRun: true);
|
||||
applyMocksToCommand(command);
|
||||
MockDeviceStore mockDevices = command.devices;
|
||||
|
||||
when(mockDevices.android.isConnected()).thenReturn(false);
|
||||
when(mockDevices.iOS.isConnected()).thenReturn(false);
|
||||
when(mockDevices.iOSSimulator.isConnected()).thenReturn(false);
|
||||
|
||||
CommandRunner runner = new CommandRunner('test_flutter', '')
|
||||
..addCommand(command);
|
||||
runner.run(['listen']).then((int code) => expect(code, equals(0)));
|
||||
});
|
||||
});
|
||||
}
|
30
packages/flutter_tools/test/logs_test.dart
Normal file
30
packages/flutter_tools/test/logs_test.dart
Normal file
@ -0,0 +1,30 @@
|
||||
// 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 'package:args/command_runner.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
import 'package:sky_tools/src/commands/logs.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import 'src/mocks.dart';
|
||||
|
||||
main() => defineTests();
|
||||
|
||||
defineTests() {
|
||||
group('logs', () {
|
||||
test('returns 0 when no device is connected', () {
|
||||
LogsCommand command = new LogsCommand();
|
||||
applyMocksToCommand(command);
|
||||
MockDeviceStore mockDevices = command.devices;
|
||||
|
||||
when(mockDevices.android.isConnected()).thenReturn(false);
|
||||
when(mockDevices.iOS.isConnected()).thenReturn(false);
|
||||
when(mockDevices.iOSSimulator.isConnected()).thenReturn(false);
|
||||
|
||||
CommandRunner runner = new CommandRunner('test_flutter', '')
|
||||
..addCommand(command);
|
||||
runner.run(['logs']).then((int code) => expect(code, equals(0)));
|
||||
});
|
||||
});
|
||||
}
|
66
packages/flutter_tools/test/os_utils_test.dart
Normal file
66
packages/flutter_tools/test/os_utils_test.dart
Normal file
@ -0,0 +1,66 @@
|
||||
// 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:io';
|
||||
|
||||
import 'package:sky_tools/src/os_utils.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
main() => defineTests();
|
||||
|
||||
defineTests() {
|
||||
group('OperatingSystemUtils', () {
|
||||
Directory temp;
|
||||
|
||||
setUp(() {
|
||||
temp = Directory.systemTemp.createTempSync('sky_tools');
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
temp.deleteSync(recursive: true);
|
||||
});
|
||||
|
||||
test('makeExecutable', () {
|
||||
File file = new File(p.join(temp.path, 'foo.script'));
|
||||
file.writeAsStringSync('hello world');
|
||||
osUtils.makeExecutable(file);
|
||||
|
||||
// Skip this test on windows.
|
||||
if (!Platform.isWindows) {
|
||||
String mode = file.statSync().modeString();
|
||||
// rwxr--r--
|
||||
expect(mode.substring(0, 3), endsWith('x'));
|
||||
}
|
||||
});
|
||||
|
||||
/// Start a script listening on a port, try and kill that process.
|
||||
test('killTcpPortListeners', () async {
|
||||
final int port = 40170;
|
||||
|
||||
File file = new File(p.join(temp.path, 'script.dart'));
|
||||
file.writeAsStringSync('''
|
||||
import 'dart:io';
|
||||
|
||||
void main() async {
|
||||
ServerSocket serverSocket = await ServerSocket.bind(
|
||||
InternetAddress.LOOPBACK_IP_V4, ${port});
|
||||
// wait...
|
||||
print('listening on port ${port}...');
|
||||
}
|
||||
''');
|
||||
Process process = await Process.start('dart', [file.path]);
|
||||
await process.stdout.first;
|
||||
|
||||
osUtils.killTcpPortListeners(40170);
|
||||
int exitCode = await process.exitCode;
|
||||
expect(exitCode, isNot(equals(0)));
|
||||
});
|
||||
|
||||
/// Try and kill with a port that no process is listening to.
|
||||
test('killTcpPortListeners none', () {
|
||||
osUtils.killTcpPortListeners(40171);
|
||||
});
|
||||
});
|
||||
}
|
61
packages/flutter_tools/test/src/mocks.dart
Normal file
61
packages/flutter_tools/test/src/mocks.dart
Normal file
@ -0,0 +1,61 @@
|
||||
// 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 'package:mockito/mockito.dart';
|
||||
import 'package:sky_tools/src/application_package.dart';
|
||||
import 'package:sky_tools/src/build_configuration.dart';
|
||||
import 'package:sky_tools/src/commands/flutter_command.dart';
|
||||
import 'package:sky_tools/src/device.dart';
|
||||
import 'package:sky_tools/src/toolchain.dart';
|
||||
|
||||
class MockApplicationPackageStore extends ApplicationPackageStore {
|
||||
MockApplicationPackageStore() : super(
|
||||
android: new AndroidApk(localPath: '/mock/path/to/android/SkyShell.apk'),
|
||||
iOS: new IOSApp(localPath: '/mock/path/to/iOS/SkyShell.app'),
|
||||
iOSSimulator: new IOSApp(localPath: '/mock/path/to/iOSSimulator/SkyShell.app'));
|
||||
}
|
||||
|
||||
class MockCompiler extends Mock implements Compiler {
|
||||
@override
|
||||
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
||||
}
|
||||
|
||||
class MockToolchain extends Toolchain {
|
||||
MockToolchain() : super(compiler: new MockCompiler());
|
||||
}
|
||||
|
||||
class MockAndroidDevice extends Mock implements AndroidDevice {
|
||||
TargetPlatform get platform => TargetPlatform.android;
|
||||
|
||||
@override
|
||||
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
||||
}
|
||||
|
||||
class MockIOSDevice extends Mock implements IOSDevice {
|
||||
TargetPlatform get platform => TargetPlatform.iOS;
|
||||
|
||||
@override
|
||||
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
||||
}
|
||||
|
||||
class MockIOSSimulator extends Mock implements IOSSimulator {
|
||||
TargetPlatform get platform => TargetPlatform.iOSSimulator;
|
||||
|
||||
@override
|
||||
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
||||
}
|
||||
|
||||
class MockDeviceStore extends DeviceStore {
|
||||
MockDeviceStore() : super(
|
||||
android: new MockAndroidDevice(),
|
||||
iOS: new MockIOSDevice(),
|
||||
iOSSimulator: new MockIOSSimulator());
|
||||
}
|
||||
|
||||
void applyMocksToCommand(FlutterCommand command) {
|
||||
command
|
||||
..applicationPackages = new MockApplicationPackageStore()
|
||||
..toolchain = new MockToolchain()
|
||||
..devices = new MockDeviceStore();
|
||||
}
|
72
packages/flutter_tools/test/start_test.dart
Normal file
72
packages/flutter_tools/test/start_test.dart
Normal file
@ -0,0 +1,72 @@
|
||||
// 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 'package:args/command_runner.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
import 'package:sky_tools/src/commands/start.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import 'src/mocks.dart';
|
||||
|
||||
main() => defineTests();
|
||||
|
||||
defineTests() {
|
||||
group('start', () {
|
||||
test('returns 0 when Android is connected and ready to be started', () {
|
||||
StartCommand command = new StartCommand();
|
||||
applyMocksToCommand(command);
|
||||
MockDeviceStore mockDevices = command.devices;
|
||||
|
||||
when(mockDevices.android.isConnected()).thenReturn(true);
|
||||
when(mockDevices.android.isAppInstalled(any)).thenReturn(false);
|
||||
when(mockDevices.android.installApp(any)).thenReturn(true);
|
||||
when(mockDevices.android.startBundle(any, any, any, any)).thenReturn(true);
|
||||
when(mockDevices.android.stopApp(any)).thenReturn(true);
|
||||
|
||||
when(mockDevices.iOS.isConnected()).thenReturn(false);
|
||||
when(mockDevices.iOS.isAppInstalled(any)).thenReturn(false);
|
||||
when(mockDevices.iOS.installApp(any)).thenReturn(false);
|
||||
when(mockDevices.iOS.startApp(any)).thenReturn(false);
|
||||
when(mockDevices.iOS.stopApp(any)).thenReturn(false);
|
||||
|
||||
when(mockDevices.iOSSimulator.isConnected()).thenReturn(false);
|
||||
when(mockDevices.iOSSimulator.isAppInstalled(any)).thenReturn(false);
|
||||
when(mockDevices.iOSSimulator.installApp(any)).thenReturn(false);
|
||||
when(mockDevices.iOSSimulator.startApp(any)).thenReturn(false);
|
||||
when(mockDevices.iOSSimulator.stopApp(any)).thenReturn(false);
|
||||
|
||||
CommandRunner runner = new CommandRunner('test_flutter', '')
|
||||
..addCommand(command);
|
||||
runner.run(['start']).then((int code) => expect(code, equals(0)));
|
||||
});
|
||||
|
||||
test('returns 0 when iOS is connected and ready to be started', () {
|
||||
StartCommand command = new StartCommand();
|
||||
applyMocksToCommand(command);
|
||||
MockDeviceStore mockDevices = command.devices;
|
||||
|
||||
when(mockDevices.android.isConnected()).thenReturn(false);
|
||||
when(mockDevices.android.isAppInstalled(any)).thenReturn(false);
|
||||
when(mockDevices.android.installApp(any)).thenReturn(false);
|
||||
when(mockDevices.android.startBundle(any, any, any, any)).thenReturn(false);
|
||||
when(mockDevices.android.stopApp(any)).thenReturn(false);
|
||||
|
||||
when(mockDevices.iOS.isConnected()).thenReturn(true);
|
||||
when(mockDevices.iOS.isAppInstalled(any)).thenReturn(false);
|
||||
when(mockDevices.iOS.installApp(any)).thenReturn(true);
|
||||
when(mockDevices.iOS.startApp(any)).thenReturn(true);
|
||||
when(mockDevices.iOS.stopApp(any)).thenReturn(false);
|
||||
|
||||
when(mockDevices.iOSSimulator.isConnected()).thenReturn(false);
|
||||
when(mockDevices.iOSSimulator.isAppInstalled(any)).thenReturn(false);
|
||||
when(mockDevices.iOSSimulator.installApp(any)).thenReturn(false);
|
||||
when(mockDevices.iOSSimulator.startApp(any)).thenReturn(false);
|
||||
when(mockDevices.iOSSimulator.stopApp(any)).thenReturn(false);
|
||||
|
||||
CommandRunner runner = new CommandRunner('test_flutter', '')
|
||||
..addCommand(command);
|
||||
runner.run(['start']).then((int code) => expect(code, equals(0)));
|
||||
});
|
||||
});
|
||||
}
|
54
packages/flutter_tools/test/stop_test.dart
Normal file
54
packages/flutter_tools/test/stop_test.dart
Normal file
@ -0,0 +1,54 @@
|
||||
// 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 'package:args/command_runner.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
import 'package:sky_tools/src/commands/stop.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import 'src/mocks.dart';
|
||||
|
||||
main() => defineTests();
|
||||
|
||||
defineTests() {
|
||||
group('stop', () {
|
||||
test('returns 0 when Android is connected and ready to be stopped', () {
|
||||
StopCommand command = new StopCommand();
|
||||
applyMocksToCommand(command);
|
||||
MockDeviceStore mockDevices = command.devices;
|
||||
|
||||
when(mockDevices.android.isConnected()).thenReturn(true);
|
||||
when(mockDevices.android.stopApp(any)).thenReturn(true);
|
||||
|
||||
when(mockDevices.iOS.isConnected()).thenReturn(false);
|
||||
when(mockDevices.iOS.stopApp(any)).thenReturn(false);
|
||||
|
||||
when(mockDevices.iOSSimulator.isConnected()).thenReturn(false);
|
||||
when(mockDevices.iOSSimulator.stopApp(any)).thenReturn(false);
|
||||
|
||||
CommandRunner runner = new CommandRunner('test_flutter', '')
|
||||
..addCommand(command);
|
||||
runner.run(['stop']).then((int code) => expect(code, equals(0)));
|
||||
});
|
||||
|
||||
test('returns 0 when iOS is connected and ready to be stopped', () {
|
||||
StopCommand command = new StopCommand();
|
||||
applyMocksToCommand(command);
|
||||
MockDeviceStore mockDevices = command.devices;
|
||||
|
||||
when(mockDevices.android.isConnected()).thenReturn(false);
|
||||
when(mockDevices.android.stopApp(any)).thenReturn(false);
|
||||
|
||||
when(mockDevices.iOS.isConnected()).thenReturn(true);
|
||||
when(mockDevices.iOS.stopApp(any)).thenReturn(true);
|
||||
|
||||
when(mockDevices.iOSSimulator.isConnected()).thenReturn(false);
|
||||
when(mockDevices.iOSSimulator.stopApp(any)).thenReturn(false);
|
||||
|
||||
CommandRunner runner = new CommandRunner('test_flutter', '')
|
||||
..addCommand(command);
|
||||
runner.run(['stop']).then((int code) => expect(code, equals(0)));
|
||||
});
|
||||
});
|
||||
}
|
28
packages/flutter_tools/test/trace_test.dart
Normal file
28
packages/flutter_tools/test/trace_test.dart
Normal file
@ -0,0 +1,28 @@
|
||||
// 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 'package:args/command_runner.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
import 'package:sky_tools/src/commands/trace.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import 'src/mocks.dart';
|
||||
|
||||
main() => defineTests();
|
||||
|
||||
defineTests() {
|
||||
group('trace', () {
|
||||
test('returns 1 when no Android device is connected', () {
|
||||
TraceCommand command = new TraceCommand();
|
||||
applyMocksToCommand(command);
|
||||
MockDeviceStore mockDevices = command.devices;
|
||||
|
||||
when(mockDevices.android.isConnected()).thenReturn(false);
|
||||
|
||||
CommandRunner runner = new CommandRunner('test_flutter', '')
|
||||
..addCommand(command);
|
||||
runner.run(['trace']).then((int code) => expect(code, equals(1)));
|
||||
});
|
||||
});
|
||||
}
|
49
packages/flutter_tools/tool/daemon_client.dart
Normal file
49
packages/flutter_tools/tool/daemon_client.dart
Normal file
@ -0,0 +1,49 @@
|
||||
// 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:convert';
|
||||
import 'dart:io';
|
||||
|
||||
Process daemon;
|
||||
|
||||
main() async {
|
||||
daemon = await Process.start('dart', ['bin/sky_tools.dart', 'daemon']);
|
||||
print('daemon process started, pid: ${daemon.pid}');
|
||||
|
||||
daemon.stdout
|
||||
.transform(UTF8.decoder)
|
||||
.transform(const LineSplitter())
|
||||
.listen((String line) => print('<== ${line}'));
|
||||
daemon.stderr.listen((data) => stderr.add(data));
|
||||
|
||||
stdout.write('> ');
|
||||
stdin.transform(UTF8.decoder).transform(const LineSplitter()).listen((String line) {
|
||||
if (line == 'version' || line == 'v') {
|
||||
_send({'event': 'daemon.version'});
|
||||
} else if (line == 'shutdown' || line == 'q') {
|
||||
_send({'event': 'daemon.shutdown'});
|
||||
} else if (line == 'start') {
|
||||
_send({'event': 'app.start'});
|
||||
} else if (line == 'stopAll') {
|
||||
_send({'event': 'app.stopAll'});
|
||||
} else {
|
||||
print('command not understood: ${line}');
|
||||
}
|
||||
stdout.write('> ');
|
||||
});
|
||||
|
||||
daemon.exitCode.then((int code) {
|
||||
print('daemon exiting (${code})');
|
||||
exit(code);
|
||||
});
|
||||
}
|
||||
|
||||
int id = 0;
|
||||
|
||||
void _send(Map map) {
|
||||
map['id'] = id++;
|
||||
String str = '[${JSON.encode(map)}]';
|
||||
daemon.stdin.writeln(str);
|
||||
print('==> ${str}');
|
||||
}
|
18
packages/flutter_tools/tool/travis.sh
Executable file
18
packages/flutter_tools/tool/travis.sh
Executable file
@ -0,0 +1,18 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 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.
|
||||
|
||||
# Fast fail the script on failures.
|
||||
set -e
|
||||
|
||||
# Fetch all our dependencies
|
||||
pub get
|
||||
|
||||
# Verify that the libraries are error free.
|
||||
pub global activate tuneup
|
||||
pub global run tuneup check
|
||||
|
||||
# And run our tests.
|
||||
pub run test -j1
|
@ -3,5 +3,6 @@ set -ex
|
||||
|
||||
(cd packages/cassowary; pub get)
|
||||
(cd packages/newton; pub get)
|
||||
(cd packages/flutter_tools; pub get)
|
||||
|
||||
pub global activate tuneup
|
||||
|
@ -3,3 +3,4 @@ set -ex
|
||||
|
||||
(cd packages/cassowary; pub global run tuneup check; pub run test -j1)
|
||||
(cd packages/newton; pub global run tuneup check; pub run test -j1)
|
||||
(cd packages/flutter_tools; pub global run tuneup check; pub run test -j1)
|
||||
|
Loading…
Reference in New Issue
Block a user