make the ios development path less mandatory
diff --git a/packages/flutter_tools/lib/executable.dart b/packages/flutter_tools/lib/executable.dart
index 4cf30cd..b00a798 100644
--- a/packages/flutter_tools/lib/executable.dart
+++ b/packages/flutter_tools/lib/executable.dart
@@ -18,6 +18,7 @@
import 'src/commands/create.dart';
import 'src/commands/daemon.dart';
import 'src/commands/devices.dart';
+import 'src/commands/doctor.dart';
import 'src/commands/install.dart';
import 'src/commands/ios.dart';
import 'src/commands/listen.dart';
@@ -30,6 +31,8 @@
import 'src/commands/trace.dart';
import 'src/commands/upgrade.dart';
import 'src/device.dart';
+import 'src/doctor.dart';
+import 'src/ios/mac.dart';
import 'src/runner/flutter_command_runner.dart';
/// Main entry point for commands.
@@ -48,6 +51,7 @@
..addCommand(new CreateCommand())
..addCommand(new DaemonCommand(hideCommand: !verboseHelp))
..addCommand(new DevicesCommand())
+ ..addCommand(new DoctorCommand())
..addCommand(new InstallCommand())
..addCommand(new IOSCommand())
..addCommand(new ListenCommand())
@@ -64,6 +68,8 @@
// Initialize globals.
context[Logger] = new StdoutLogger();
context[DeviceManager] = new DeviceManager();
+ Doctor.initGlobal();
+ XCode.initGlobal();
dynamic result = await runner.run(args);
diff --git a/packages/flutter_tools/lib/src/android/android_workflow.dart b/packages/flutter_tools/lib/src/android/android_workflow.dart
new file mode 100644
index 0000000..3bbc619
--- /dev/null
+++ b/packages/flutter_tools/lib/src/android/android_workflow.dart
@@ -0,0 +1,34 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import '../base/globals.dart';
+import '../doctor.dart';
+import 'android_sdk.dart';
+
+class AndroidWorkflow extends Workflow {
+ AndroidWorkflow() : super('Android');
+
+ bool get appliesToHostPlatform => true;
+
+ bool get canListDevices => getAdbPath(androidSdk) != null;
+
+ bool get canLaunchDevices => androidSdk != null && androidSdk.validateSdkWellFormed(complain: false);
+
+ void diagnose() {
+ Validator androidValidator = new Validator('Develop for Android devices');
+
+ Function _sdkExists = () {
+ return androidSdk == null ? ValidationType.missing : ValidationType.installed;
+ };
+
+ androidValidator.addValidator(new Validator(
+ 'Android SDK',
+ description: 'enable development for Android devices',
+ resolution: 'Download at https://developer.android.com/sdk/',
+ validatorFunction: _sdkExists
+ ));
+
+ androidValidator.validate().print();
+ }
+}
diff --git a/packages/flutter_tools/lib/src/base/globals.dart b/packages/flutter_tools/lib/src/base/globals.dart
index ebe3c12..d4f1398 100644
--- a/packages/flutter_tools/lib/src/base/globals.dart
+++ b/packages/flutter_tools/lib/src/base/globals.dart
@@ -4,12 +4,18 @@
import '../android/android_sdk.dart';
import '../device.dart';
+import '../doctor.dart';
+import '../ios/mac.dart';
import 'context.dart';
import 'logger.dart';
DeviceManager get deviceManager => context[DeviceManager];
Logger get logger => context[Logger];
AndroidSdk get androidSdk => context[AndroidSdk];
+Doctor get doctor => context[Doctor];
+
+// Mac specific globals - will be null on other platforms.
+XCode get xcode => context[XCode];
/// Display an error level message to the user. Commands should use this if they
/// fail in some way.
diff --git a/packages/flutter_tools/lib/src/base/process.dart b/packages/flutter_tools/lib/src/base/process.dart
index cac6702..28b4df5 100644
--- a/packages/flutter_tools/lib/src/base/process.dart
+++ b/packages/flutter_tools/lib/src/base/process.dart
@@ -80,6 +80,14 @@
return Platform.isWindows ? '$name.bat' : name;
}
+bool exitsHappy(List<String> cli) {
+ try {
+ return Process.runSync(cli.first, cli.sublist(1)).exitCode == 0;
+ } catch (error) {
+ return false;
+ }
+}
+
String _runWithLoggingSync(List<String> cmd, {
bool checked: false,
bool noisyErrors: false,
diff --git a/packages/flutter_tools/lib/src/commands/devices.dart b/packages/flutter_tools/lib/src/commands/devices.dart
index 072e9f0..3ff2697 100644
--- a/packages/flutter_tools/lib/src/commands/devices.dart
+++ b/packages/flutter_tools/lib/src/commands/devices.dart
@@ -16,6 +16,12 @@
bool get requiresProjectRoot => false;
Future<int> runInProject() async {
+ if (!doctor.canListAnything) {
+ printError("Unable to locate a development device; please run 'flutter doctor' for "
+ "information about installing additional components.");
+ return 1;
+ }
+
List<Device> devices = await deviceManager.getAllConnectedDevices();
if (devices.isEmpty) {
diff --git a/packages/flutter_tools/lib/src/commands/doctor.dart b/packages/flutter_tools/lib/src/commands/doctor.dart
new file mode 100644
index 0000000..ed049d0
--- /dev/null
+++ b/packages/flutter_tools/lib/src/commands/doctor.dart
@@ -0,0 +1,33 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+
+import '../artifacts.dart';
+import '../base/globals.dart';
+import '../runner/flutter_command.dart';
+import '../runner/version.dart';
+
+class DoctorCommand extends FlutterCommand {
+ final String name = 'doctor';
+ final String description = 'Diagnose the flutter tool.';
+
+ bool get requiresProjectRoot => false;
+
+ Future<int> runInProject() async {
+ // general info
+ String flutterRoot = ArtifactStore.flutterRoot;
+ printStatus('Flutter root is $flutterRoot.');
+ printStatus('');
+
+ // doctor
+ doctor.diagnose();
+ printStatus('');
+
+ // version
+ printStatus(getVersion(flutterRoot));
+
+ return 0;
+ }
+}
diff --git a/packages/flutter_tools/lib/src/doctor.dart b/packages/flutter_tools/lib/src/doctor.dart
new file mode 100644
index 0000000..4fb4386
--- /dev/null
+++ b/packages/flutter_tools/lib/src/doctor.dart
@@ -0,0 +1,154 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'android/android_workflow.dart';
+import 'base/context.dart';
+import 'base/globals.dart';
+import 'ios/ios_workflow.dart';
+
+class Doctor {
+ Doctor() {
+ _iosWorkflow = new IOSWorkflow();
+ if (_iosWorkflow.appliesToHostPlatform)
+ _workflows.add(_iosWorkflow);
+
+ _androidWorkflow = new AndroidWorkflow();
+ if (_androidWorkflow.appliesToHostPlatform)
+ _workflows.add(_androidWorkflow);
+ }
+
+ static void initGlobal() {
+ context[Doctor] = new Doctor();
+ }
+
+ IOSWorkflow _iosWorkflow;
+ AndroidWorkflow _androidWorkflow;
+
+ /// This can return null for platforms that don't support developing for iOS.
+ IOSWorkflow get iosWorkflow => _iosWorkflow;
+
+ AndroidWorkflow get androidWorkflow => _androidWorkflow;
+
+ List<Workflow> _workflows = <Workflow>[];
+
+ List<Workflow> get workflows => _workflows;
+
+ /// Print verbose information about the state of installed tooling.
+ void diagnose() {
+ for (int i = 0; i < workflows.length; i++) {
+ if (i > 0)
+ printStatus('');
+ workflows[i].diagnose();
+ }
+ }
+
+ bool get canListAnything => workflows.any((Workflow workflow) => workflow.canListDevices);
+
+ bool get canLaunchAnything => workflows.any((Workflow workflow) => workflow.canLaunchDevices);
+}
+
+/// A series of tools and required install steps for a target platform (iOS or Android).
+abstract class Workflow {
+ Workflow(this.name);
+
+ final String name;
+
+ /// Whether the workflow applies to this platform (as in, should we ever try and use it).
+ bool get appliesToHostPlatform;
+
+ /// Are we functional enough to list devices?
+ bool get canListDevices;
+
+ /// Could this thing launch *something*? It may still have minor issues.
+ bool get canLaunchDevices;
+
+ /// Print verbose information about the state of the workflow.
+ void diagnose();
+
+ String toString() => name;
+}
+
+enum ValidationType {
+ missing,
+ partial,
+ installed
+}
+
+typedef ValidationType ValidationFunction();
+
+class Validator {
+ Validator(this.name, { this.description, this.resolution, this.validatorFunction });
+
+ final String name;
+ final String description;
+ final String resolution;
+ final ValidationFunction validatorFunction;
+
+ List<Validator> _children = [];
+
+ ValidationResult validate() {
+ if (validatorFunction != null)
+ return new ValidationResult(validatorFunction(), this);
+
+ List<ValidationResult> results = _children.map((Validator child) {
+ return child.validate();
+ }).toList();
+
+ ValidationType type = _combine(results.map((ValidationResult result) {
+ return result.type;
+ }));
+ return new ValidationResult(type, this, results);
+ }
+
+ ValidationType _combine(Iterable<ValidationType> types) {
+ if (types.contains(ValidationType.missing) && types.contains(ValidationType.installed))
+ return ValidationType.partial;
+ if (types.contains(ValidationType.missing))
+ return ValidationType.missing;
+ return ValidationType.installed;
+ }
+
+ void addValidator(Validator validator) => _children.add(validator);
+}
+
+class ValidationResult {
+ ValidationResult(this.type, this.validator, [this.childResults = const <ValidationResult>[]]);
+
+ final ValidationType type;
+ final Validator validator;
+ final List<ValidationResult> childResults;
+
+ void print([String indent = '']) {
+ printSelf(indent);
+
+ for (ValidationResult child in childResults)
+ child.print(indent + ' ');
+ }
+
+ void printSelf(String indent) {
+ String result = indent;
+
+ if (type == ValidationType.missing)
+ result += '[ ] ';
+ else if (type == ValidationType.installed)
+ result += '[✓] ';
+ else
+ result += '[-] ';
+
+ result += '${validator.name} ';
+
+ if (validator.description != null)
+ result += '- ${validator.description} ';
+
+ if (type == ValidationType.missing)
+ result += '(missing)';
+ else if (type == ValidationType.installed)
+ result += '(installed)';
+
+ printStatus(result);
+
+ if (type == ValidationType.missing && validator.resolution != null)
+ printStatus('$indent ${validator.resolution}');
+ }
+}
diff --git a/packages/flutter_tools/lib/src/ios/device_ios.dart b/packages/flutter_tools/lib/src/ios/device_ios.dart
index f705fd0..908cb3e 100644
--- a/packages/flutter_tools/lib/src/ios/device_ios.dart
+++ b/packages/flutter_tools/lib/src/ios/device_ios.dart
@@ -86,6 +86,9 @@
bool get supportsStartPaused => false;
static List<IOSDevice> getAttachedDevices([IOSDevice mockIOS]) {
+ if (!doctor.iosWorkflow.hasIdeviceId)
+ return <IOSDevice>[];
+
List<IOSDevice> devices = [];
for (String id in _getAttachedDeviceIDs(mockIOS)) {
String name = _getDeviceName(id, mockIOS);
@@ -176,7 +179,7 @@
// Step 1: Install the precompiled application if necessary
bool buildResult = await _buildIOSXcodeProject(app, buildForDevice: true);
if (!buildResult) {
- printError('Could not build the precompiled application for the device');
+ printError('Could not build the precompiled application for the device.');
return false;
}
@@ -184,7 +187,7 @@
Directory bundle = new Directory(path.join(app.localPath, 'build', 'Release-iphoneos', 'Runner.app'));
bool bundleExists = bundle.existsSync();
if (!bundleExists) {
- printError('Could not find the built application bundle at ${bundle.path}');
+ printError('Could not find the built application bundle at ${bundle.path}.');
return false;
}
@@ -202,7 +205,7 @@
]);
if (installationResult != 0) {
- printError('Could not install ${bundle.path} on $id');
+ printError('Could not install ${bundle.path} on $id.');
return false;
}
@@ -246,6 +249,9 @@
IOSSimulator(String id, { this.name }) : super(id);
static List<IOSSimulator> getAttachedDevices() {
+ if (!xcode.isInstalled)
+ return <IOSSimulator>[];
+
return SimControl.getConnectedDevices().map((SimDevice device) {
return new IOSSimulator(device.udid, name: device.name);
}).toList();
@@ -317,7 +323,7 @@
// Step 1: Build the Xcode project
bool buildResult = await _buildIOSXcodeProject(app, buildForDevice: false);
if (!buildResult) {
- printError('Could not build the application for the simulator');
+ printError('Could not build the application for the simulator.');
return false;
}
@@ -325,7 +331,7 @@
Directory bundle = new Directory(path.join(app.localPath, 'build', 'Release-iphonesimulator', 'Runner.app'));
bool bundleExists = await bundle.exists();
if (!bundleExists) {
- printError('Could not find the built application bundle at ${bundle.path}');
+ printError('Could not find the built application bundle at ${bundle.path}.');
return false;
}
@@ -476,7 +482,8 @@
String category = match.group(1);
String content = match.group(2);
if (category == 'Game Center' || category == 'itunesstored' || category == 'nanoregistrylaunchd' ||
- category == 'mstreamd' || category == 'syncdefaultsd' || category == 'companionappd' || category == 'searchd')
+ category == 'mstreamd' || category == 'syncdefaultsd' || category == 'companionappd' ||
+ category == 'searchd')
return null;
_lastWasFiltered = false;
diff --git a/packages/flutter_tools/lib/src/ios/ios_workflow.dart b/packages/flutter_tools/lib/src/ios/ios_workflow.dart
new file mode 100644
index 0000000..8dfc94f
--- /dev/null
+++ b/packages/flutter_tools/lib/src/ios/ios_workflow.dart
@@ -0,0 +1,82 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:io';
+
+import '../base/globals.dart';
+import '../base/process.dart';
+import '../doctor.dart';
+
+class IOSWorkflow extends Workflow {
+ IOSWorkflow() : super('iOS');
+
+ bool get appliesToHostPlatform => Platform.isMacOS;
+
+ // We need xcode (+simctl) to list simulator devices, and idevice_id to list real devices.
+ bool get canListDevices => xcode.isInstalled;
+
+ // We need xcode to launch simulator devices, and ideviceinstaller and ios-deploy
+ // for real devices.
+ bool get canLaunchDevices => xcode.isInstalled;
+
+ void diagnose() {
+ Validator iosValidator = new Validator('Develop for iOS devices');
+
+ Function _xcodeExists = () {
+ return xcode.isInstalled ? ValidationType.installed : ValidationType.missing;
+ };
+
+ Function _brewExists = () {
+ return exitsHappy(<String>['brew', '-v'])
+ ? ValidationType.installed : ValidationType.missing;
+ };
+
+ Function _ideviceinstallerExists = () {
+ return exitsHappy(<String>['ideviceinstaller', '-h'])
+ ? ValidationType.installed : ValidationType.missing;
+ };
+
+ Function _iosdeployExists = () {
+ return hasIdeviceId ? ValidationType.installed : ValidationType.missing;
+ };
+
+ iosValidator.addValidator(new Validator(
+ 'XCode',
+ description: 'enable development for iOS devices',
+ resolution: 'Download at https://developer.apple.com/xcode/download/',
+ validatorFunction: _xcodeExists
+ ));
+
+ iosValidator.addValidator(new Validator(
+ 'brew',
+ description: 'install additional development packages',
+ resolution: 'Download at http://brew.sh/',
+ validatorFunction: _brewExists
+ ));
+
+ iosValidator.addValidator(new Validator(
+ 'ideviceinstaller',
+ description: 'discover connected iOS devices',
+ resolution: "Install via 'brew install ideviceinstaller'",
+ validatorFunction: _ideviceinstallerExists
+ ));
+
+ iosValidator.addValidator(new Validator(
+ 'ios-deploy',
+ description: 'deploy to connected iOS devices',
+ resolution: "Install via 'brew install ios-deploy'",
+ validatorFunction: _iosdeployExists
+ ));
+
+ iosValidator.validate().print();
+ }
+
+ bool get hasIdeviceId => exitsHappy(<String>['idevice_id', '-h']);
+
+ /// Return whether the tooling to list and deploy to real iOS devices (not the
+ /// simulator) is installed on the user's machine.
+ bool get canWorkWithIOSDevices {
+ return exitsHappy(<String>['ideviceinstaller', '-h']) && hasIdeviceId;
+ }
+}
diff --git a/packages/flutter_tools/lib/src/ios/mac.dart b/packages/flutter_tools/lib/src/ios/mac.dart
new file mode 100644
index 0000000..47f5e79
--- /dev/null
+++ b/packages/flutter_tools/lib/src/ios/mac.dart
@@ -0,0 +1,14 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import '../base/context.dart';
+import '../base/process.dart';
+
+class XCode {
+ static void initGlobal() {
+ context[XCode] = new XCode();
+ }
+
+ bool get isInstalled => exitsHappy(<String>['xcode-select', '--print-path']);
+}
diff --git a/packages/flutter_tools/test/src/context.dart b/packages/flutter_tools/test/src/context.dart
index b1c518c..4b9cd38 100644
--- a/packages/flutter_tools/test/src/context.dart
+++ b/packages/flutter_tools/test/src/context.dart
@@ -3,10 +3,13 @@
// found in the LICENSE file.
import 'dart:async';
+import 'dart:io';
import 'package:flutter_tools/src/base/context.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/device.dart';
+import 'package:flutter_tools/src/doctor.dart';
+import 'package:flutter_tools/src/ios/mac.dart';
import 'package:test/test.dart';
/// Return the test logger. This assumes that the current Logger is a BufferLogger.
@@ -29,6 +32,14 @@
if (!overrides.containsKey(DeviceManager))
testContext[DeviceManager] = new MockDeviceManager();
+ if (!overrides.containsKey(Doctor))
+ testContext[Doctor] = new Doctor();
+
+ if (Platform.isMacOS) {
+ if (!overrides.containsKey(XCode))
+ testContext[XCode] = new XCode();
+ }
+
return testContext.runInZone(testMethod);
}, timeout: timeout);
}