// Copyright 2014 The Flutter 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:meta/meta.dart'; import '../../src/macos/xcode.dart'; import '../base/common.dart'; import '../base/file_system.dart'; import '../base/logger.dart'; import '../build_info.dart'; import '../globals.dart' as globals; import '../ios/xcodeproj.dart'; import '../project.dart'; import '../runner/flutter_command.dart'; class CleanCommand extends FlutterCommand { CleanCommand({ bool verbose = false, }) : _verbose = verbose { requiresPubspecYaml(); argParser.addOption( 'scheme', help: 'When cleaning Xcode schemes, clean only the specified scheme.', ); } final bool _verbose; @override final String name = 'clean'; @override final String description = 'Delete the build/ and .dart_tool/ directories.'; @override String get category => FlutterCommandCategory.project; @override Future<Set<DevelopmentArtifact>> get requiredArtifacts async => const <DevelopmentArtifact>{}; @override Future<FlutterCommandResult> runCommand() async { // Clean Xcode to remove intermediate DerivedData artifacts. // Do this before removing ephemeral directory, which would delete the xcworkspace. final FlutterProject flutterProject = FlutterProject.current(); final Xcode? xcode = globals.xcode; if (xcode != null && xcode.isInstalledAndMeetsVersionCheck) { await _cleanXcode(flutterProject.ios); await _cleanXcode(flutterProject.macos); } final Directory buildDir = globals.fs.directory(getBuildDirectory()); deleteFile(buildDir); deleteFile(flutterProject.dartTool); deleteFile(flutterProject.packagesFile); deleteFile(flutterProject.android.ephemeralDirectory); deleteFile(flutterProject.ios.ephemeralDirectory); deleteFile(flutterProject.ios.ephemeralModuleDirectory); deleteFile(flutterProject.ios.generatedXcodePropertiesFile); deleteFile(flutterProject.ios.generatedEnvironmentVariableExportScript); deleteFile(flutterProject.ios.deprecatedCompiledDartFramework); deleteFile(flutterProject.ios.deprecatedProjectFlutterFramework); deleteFile(flutterProject.ios.flutterPodspec); deleteFile(flutterProject.linux.ephemeralDirectory); deleteFile(flutterProject.macos.ephemeralDirectory); deleteFile(flutterProject.windows.ephemeralDirectory); deleteFile(flutterProject.flutterPluginsDependenciesFile); deleteFile(flutterProject.flutterPluginsFile); return const FlutterCommandResult(ExitStatus.success); } Future<void> _cleanXcode(XcodeBasedProject xcodeProject) async { final Directory? xcodeWorkspace = xcodeProject.xcodeWorkspace; if (xcodeWorkspace == null) { return; } final Status xcodeStatus = globals.logger.startProgress( 'Cleaning Xcode workspace...', ); try { final XcodeProjectInterpreter xcodeProjectInterpreter = globals.xcodeProjectInterpreter!; final XcodeProjectInfo projectInfo = (await xcodeProjectInterpreter.getInfo(xcodeWorkspace.parent.path))!; if (argResults?.wasParsed('scheme') ?? false) { final String scheme = argResults!['scheme'] as String; if (scheme.isEmpty) { throwToolExit('No scheme was specified for --scheme'); } if (!projectInfo.schemes.contains(scheme)) { throwToolExit('Scheme "$scheme" not found in ${projectInfo.schemes}'); } await xcodeProjectInterpreter.cleanWorkspace(xcodeWorkspace.path, scheme, verbose: _verbose); } else { for (final String scheme in projectInfo.schemes) { await xcodeProjectInterpreter.cleanWorkspace(xcodeWorkspace.path, scheme, verbose: _verbose); } } } on Exception catch (error) { final String message = 'Could not clean Xcode workspace: $error'; if (argResults?.wasParsed('scheme') ?? false) { throwToolExit(message); } else { globals.printTrace(message); } } finally { xcodeStatus.stop(); } } @visibleForTesting void deleteFile(FileSystemEntity file) { // This will throw a FileSystemException if the directory is missing permissions. try { if (!file.existsSync()) { return; } } on FileSystemException catch (err) { globals.printError('Cannot clean ${file.path}.\n$err'); return; } final Status deletionStatus = globals.logger.startProgress( 'Deleting ${file.basename}...', ); try { file.deleteSync(recursive: true); } on FileSystemException catch (error) { final String path = file.path; if (globals.platform.isWindows) { globals.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'); } else { globals.printError('Failed to remove $path: $error'); } } finally { deletionStatus.stop(); } } }