Clean Xcode workspace during flutter clean (#38992)

diff --git a/packages/flutter_tools/lib/src/commands/clean.dart b/packages/flutter_tools/lib/src/commands/clean.dart
index 7121134..5603c28 100644
--- a/packages/flutter_tools/lib/src/commands/clean.dart
+++ b/packages/flutter_tools/lib/src/commands/clean.dart
@@ -4,11 +4,15 @@
 
 import 'dart:async';
 
-import '../base/common.dart';
+import 'package:meta/meta.dart';
+
 import '../base/file_system.dart';
+import '../base/logger.dart';
 import '../base/platform.dart';
 import '../build_info.dart';
 import '../globals.dart';
+import '../ios/xcodeproj.dart';
+import '../macos/xcode.dart';
 import '../project.dart';
 import '../runner/flutter_command.dart';
 
@@ -28,38 +32,70 @@
 
   @override
   Future<FlutterCommandResult> runCommand() async {
-    final Directory buildDir = fs.directory(getBuildDirectory());
-    _deleteFile(buildDir);
-
+    // Clean Xcode to remove intermediate DerivedData artifacts.
+    // Do this before removing ephemeral directory, which would delete the xcworkspace.
     final FlutterProject flutterProject = FlutterProject.current();
-    _deleteFile(flutterProject.dartTool);
+    if (xcode.isInstalledAndMeetsVersionCheck) {
+      await _cleanXcode(flutterProject.ios);
+      await _cleanXcode(flutterProject.macos);
+    }
+
+    final Directory buildDir = fs.directory(getBuildDirectory());
+    deleteFile(buildDir);
+
+    deleteFile(flutterProject.dartTool);
 
     final Directory androidEphemeralDirectory = flutterProject.android.ephemeralDirectory;
-    _deleteFile(androidEphemeralDirectory);
+    deleteFile(androidEphemeralDirectory);
 
     final Directory iosEphemeralDirectory = flutterProject.ios.ephemeralDirectory;
-    _deleteFile(iosEphemeralDirectory);
+    deleteFile(iosEphemeralDirectory);
+
+    final Directory macosEphemeralDirectory = flutterProject.macos.ephemeralDirectory;
+    deleteFile(macosEphemeralDirectory);
 
     return const FlutterCommandResult(ExitStatus.success);
   }
 
-  void _deleteFile(FileSystemEntity file) {
-    final String path = file.path;
-    printStatus("Deleting '$path${fs.path.separator}'.");
-    if (file.existsSync()) {
-      try {
-        file.deleteSync(recursive: true);
-      } on FileSystemException catch (error) {
-        if (platform.isWindows) {
-          printError(
-            'Failed to remove $path. '
+  Future<void> _cleanXcode(XcodeBasedProject xcodeProject) async {
+    if (!xcodeProject.existsSync()) {
+      return;
+    }
+    final Status xcodeStatus = logger.startProgress('Cleaning Xcode workspace...', timeout: timeoutConfiguration.slowOperation);
+    try {
+      final Directory xcodeWorkspace = xcodeProject.xcodeWorkspace;
+      final XcodeProjectInfo projectInfo = await xcodeProjectInterpreter.getInfo(xcodeWorkspace.parent.path);
+      for (String scheme in projectInfo.schemes) {
+        xcodeProjectInterpreter.cleanWorkspace(xcodeWorkspace.path, scheme);
+      }
+    } catch (error) {
+      printTrace('Could not clean Xcode workspace: $error');
+    } finally {
+      xcodeStatus?.stop();
+    }
+  }
+
+  @visibleForTesting
+  void deleteFile(FileSystemEntity file) {
+    if (!file.existsSync()) {
+      return;
+    }
+    final Status deletionStatus = logger.startProgress('Deleting ${file.basename}...', timeout: timeoutConfiguration.fastOperation);
+    try {
+      file.deleteSync(recursive: true);
+    } on FileSystemException catch (error) {
+      final String path = file.path;
+      if (platform.isWindows) {
+        printError(
+          'Failed to remove $path. '
             'A program may still be using a file in the directory or the directory itself. '
             'To find and stop such a program, see: '
             'https://superuser.com/questions/1333118/cant-delete-empty-folder-because-it-is-used');
-        }
-        throwToolExit(error.toString());
+      } else {
+        printError('Failed to remove $path: $error');
       }
+    } finally {
+      deletionStatus.stop();
     }
   }
 }
-
diff --git a/packages/flutter_tools/lib/src/commands/run.dart b/packages/flutter_tools/lib/src/commands/run.dart
index 12d7c8e..834e03f 100644
--- a/packages/flutter_tools/lib/src/commands/run.dart
+++ b/packages/flutter_tools/lib/src/commands/run.dart
@@ -15,7 +15,6 @@
 import '../device.dart';
 import '../features.dart';
 import '../globals.dart';
-import '../macos/xcode.dart';
 import '../project.dart';
 import '../reporting/reporting.dart';
 import '../resident_runner.dart';
@@ -237,19 +236,6 @@
   }
 
   @override
-  void printNoConnectedDevices() {
-    super.printNoConnectedDevices();
-    if (getCurrentHostPlatform() == HostPlatform.darwin_x64 &&
-        xcode.isInstalledAndMeetsVersionCheck) {
-      printStatus('');
-      printStatus("Run 'flutter emulators' to list and start any available device emulators.");
-      printStatus('');
-      printStatus('If you expected your device to be detected, please run "flutter doctor" to diagnose');
-      printStatus('potential issues, or visit https://flutter.dev/setup/ for troubleshooting tips.');
-    }
-  }
-
-  @override
   bool get shouldRunPub {
     // If we are running with a prebuilt application, do not run pub.
     if (runningWithPrebuiltApplication)
diff --git a/packages/flutter_tools/lib/src/ios/xcodeproj.dart b/packages/flutter_tools/lib/src/ios/xcodeproj.dart
index bd9b6d1..ccd5c02 100644
--- a/packages/flutter_tools/lib/src/ios/xcodeproj.dart
+++ b/packages/flutter_tools/lib/src/ios/xcodeproj.dart
@@ -266,6 +266,18 @@
     }
   }
 
+  void cleanWorkspace(String workspacePath, String scheme) {
+    runSync(<String>[
+      _executable,
+      '-workspace',
+      workspacePath,
+      '-scheme',
+      scheme,
+      '-quiet',
+      'clean'
+    ], workingDirectory: fs.currentDirectory.path);
+  }
+
   Future<XcodeProjectInfo> getInfo(String projectPath) async {
     final RunResult result = await runCheckedAsync(<String>[
       _executable, '-list',
diff --git a/packages/flutter_tools/lib/src/macos/xcode.dart b/packages/flutter_tools/lib/src/macos/xcode.dart
index 7b3b915..6fea9ad 100644
--- a/packages/flutter_tools/lib/src/macos/xcode.dart
+++ b/packages/flutter_tools/lib/src/macos/xcode.dart
@@ -7,6 +7,7 @@
 import '../base/context.dart';
 import '../base/file_system.dart';
 import '../base/io.dart';
+import '../base/platform.dart';
 import '../base/process.dart';
 import '../base/process_manager.dart';
 import '../ios/xcodeproj.dart';
@@ -17,7 +18,7 @@
 Xcode get xcode => context.get<Xcode>();
 
 class Xcode {
-  bool get isInstalledAndMeetsVersionCheck => isInstalled && isVersionSatisfactory;
+  bool get isInstalledAndMeetsVersionCheck => platform.isMacOS && isInstalled && isVersionSatisfactory;
 
   String _xcodeSelectPath;
   String get xcodeSelectPath {
diff --git a/packages/flutter_tools/lib/src/runner/flutter_command.dart b/packages/flutter_tools/lib/src/runner/flutter_command.dart
index dd23247..c82a00d 100644
--- a/packages/flutter_tools/lib/src/runner/flutter_command.dart
+++ b/packages/flutter_tools/lib/src/runner/flutter_command.dart
@@ -555,10 +555,6 @@
     return deviceList.single;
   }
 
-  void printNoConnectedDevices() {
-    printStatus(userMessages.flutterNoConnectedDevices);
-  }
-
   @protected
   @mustCallSuper
   Future<void> validateCommand() async {