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);
 }