Clean Xcode workspace during flutter clean (#38992)

This commit is contained in:
Jenn Magder 2019-08-21 18:42:56 -07:00 committed by GitHub
parent aa41088e76
commit 892d62f03a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 210 additions and 91 deletions

View file

@ -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 @@ class CleanCommand extends FlutterCommand {
@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();
}
}
}

View file

@ -15,7 +15,6 @@ import '../cache.dart';
import '../device.dart';
import '../features.dart';
import '../globals.dart';
import '../macos/xcode.dart';
import '../project.dart';
import '../reporting/reporting.dart';
import '../resident_runner.dart';
@ -236,19 +235,6 @@ class RunCommand extends RunCommandBase {
};
}
@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.

View file

@ -266,6 +266,18 @@ class XcodeProjectInterpreter {
}
}
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',

View file

@ -7,6 +7,7 @@ import 'dart:async';
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 @@ const int kXcodeRequiredVersionMinor = 0;
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 {

View file

@ -555,10 +555,6 @@ abstract class FlutterCommand extends Command<void> {
return deviceList.single;
}
void printNoConnectedDevices() {
printStatus(userMessages.flutterNoConnectedDevices);
}
@protected
@mustCallSuper
Future<void> validateCommand() async {

View file

@ -2,89 +2,95 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter_tools/src/base/common.dart';
import 'package:flutter_tools/src/base/config.dart';
import 'package:file/memory.dart';
import 'package:flutter_tools/src/base/context.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/commands/clean.dart';
import 'package:flutter_tools/src/ios/xcodeproj.dart';
import 'package:flutter_tools/src/macos/xcode.dart';
import 'package:flutter_tools/src/project.dart';
import 'package:mockito/mockito.dart';
import '../../src/common.dart';
import '../../src/context.dart';
void main() {
MockFileSystem mockFileSystem;
MockDirectory currentDirectory;
MockDirectory exampleDirectory;
MockDirectory buildDirectory;
MockDirectory dartToolDirectory;
MockDirectory androidEphemeralDirectory;
MockDirectory iosEphemeralDirectory;
MockFile pubspec;
MockFile examplePubspec;
MemoryFileSystem fs;
MockPlatform windowsPlatform;
MockXcode mockXcode;
FlutterProject projectUnderTest;
MockXcodeProjectInterpreter mockXcodeProjectInterpreter;
Directory buildDirectory;
setUp(() {
mockFileSystem = MockFileSystem();
currentDirectory = MockDirectory();
exampleDirectory = MockDirectory();
buildDirectory = MockDirectory();
dartToolDirectory = MockDirectory();
androidEphemeralDirectory = MockDirectory();
iosEphemeralDirectory = MockDirectory();
pubspec = MockFile();
examplePubspec = MockFile();
fs = MemoryFileSystem();
mockXcodeProjectInterpreter = MockXcodeProjectInterpreter();
windowsPlatform = MockPlatform();
when(mockFileSystem.currentDirectory).thenReturn(currentDirectory);
when(currentDirectory.childDirectory('example')).thenReturn(exampleDirectory);
when(currentDirectory.childFile('pubspec.yaml')).thenReturn(pubspec);
when(pubspec.path).thenReturn('/test/pubspec.yaml');
when(exampleDirectory.childFile('pubspec.yaml')).thenReturn(examplePubspec);
when(currentDirectory.childDirectory('.dart_tool')).thenReturn(dartToolDirectory);
when(currentDirectory.childDirectory('.android')).thenReturn(androidEphemeralDirectory);
when(currentDirectory.childDirectory('.ios')).thenReturn(iosEphemeralDirectory);
when(examplePubspec.path).thenReturn('/test/example/pubspec.yaml');
when(mockFileSystem.isFileSync('/test/pubspec.yaml')).thenReturn(false);
when(mockFileSystem.isFileSync('/test/example/pubspec.yaml')).thenReturn(false);
when(mockFileSystem.directory('build')).thenReturn(buildDirectory);
when(mockFileSystem.path).thenReturn(fs.path);
when(buildDirectory.existsSync()).thenReturn(true);
when(dartToolDirectory.existsSync()).thenReturn(true);
when(androidEphemeralDirectory.existsSync()).thenReturn(true);
when(iosEphemeralDirectory.existsSync()).thenReturn(true);
when(windowsPlatform.isWindows).thenReturn(true);
mockXcode = MockXcode();
final Directory currentDirectory = fs.currentDirectory;
buildDirectory = currentDirectory.childDirectory('build');
buildDirectory.createSync(recursive: true);
projectUnderTest = FlutterProject.fromDirectory(currentDirectory);
projectUnderTest.ios.xcodeWorkspace.createSync(recursive: true);
projectUnderTest.macos.xcodeWorkspace.createSync(recursive: true);
projectUnderTest.dartTool.createSync(recursive: true);
projectUnderTest.android.ephemeralDirectory.createSync(recursive: true);
projectUnderTest.ios.ephemeralDirectory.createSync(recursive: true);
projectUnderTest.macos.ephemeralDirectory.createSync(recursive: true);
});
group(CleanCommand, () {
testUsingContext('removes build and .dart_tool and ephemeral directories', () async {
testUsingContext('removes build and .dart_tool and ephemeral directories, cleans Xcode', () async {
when(mockXcode.isInstalledAndMeetsVersionCheck).thenReturn(true);
await CleanCommand().runCommand();
verify(buildDirectory.deleteSync(recursive: true)).called(1);
verify(dartToolDirectory.deleteSync(recursive: true)).called(1);
verify(androidEphemeralDirectory.deleteSync(recursive: true)).called(1);
verify(iosEphemeralDirectory.deleteSync(recursive: true)).called(1);
expect(buildDirectory.existsSync(), isFalse);
expect(projectUnderTest.dartTool.existsSync(), isFalse);
expect(projectUnderTest.android.ephemeralDirectory.existsSync(), isFalse);
expect(projectUnderTest.ios.ephemeralDirectory.existsSync(), isFalse);
expect(projectUnderTest.macos.ephemeralDirectory.existsSync(), isFalse);
verify(xcodeProjectInterpreter.cleanWorkspace(any, 'Runner')).called(2);
}, overrides: <Type, Generator>{
Config: () => null,
FileSystem: () => mockFileSystem,
FileSystem: () => fs,
Xcode: () => mockXcode,
FileSystem: () => fs,
XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
});
testUsingContext('prints a helpful error message on Windows', () async {
when(mockXcode.isInstalledAndMeetsVersionCheck).thenReturn(false);
when(windowsPlatform.isWindows).thenReturn(true);
final MockFile mockFile = MockFile();
when(mockFile.existsSync()).thenReturn(true);
final BufferLogger logger = context.get<Logger>();
when(buildDirectory.deleteSync(recursive: true)).thenThrow(
const FileSystemException('Deletion failed'));
expect(() async => await CleanCommand().runCommand(), throwsA(isInstanceOf<ToolExit>()));
when(mockFile.deleteSync(recursive: true)).thenThrow(const FileSystemException('Deletion failed'));
final CleanCommand command = CleanCommand();
command.deleteFile(mockFile);
expect(logger.errorText, contains('A program may still be using a file'));
verify(mockFile.deleteSync(recursive: true)).called(1);
}, overrides: <Type, Generator>{
Config: () => null,
FileSystem: () => mockFileSystem,
Platform: () => windowsPlatform,
Logger: () => BufferLogger(),
Xcode: () => mockXcode,
});
});
}
class MockFileSystem extends Mock implements FileSystem {}
class MockFile extends Mock implements File {}
class MockDirectory extends Mock implements Directory {}
class MockPlatform extends Mock implements Platform {}
class MockXcode extends Mock implements Xcode {}
class MockXcodeProjectInterpreter extends Mock implements XcodeProjectInterpreter {
@override
Future<XcodeProjectInfo> getInfo(String projectPath) async {
return XcodeProjectInfo(null, null, <String>['Runner']);
}
}

View file

@ -3,6 +3,7 @@
// found in the LICENSE file.
import 'package:flutter_tools/src/base/io.dart' show ProcessException, ProcessResult;
import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/ios/xcodeproj.dart';
import 'package:flutter_tools/src/macos/xcode.dart';
import 'package:mockito/mockito.dart';
@ -13,17 +14,20 @@ import '../../src/context.dart';
class MockProcessManager extends Mock implements ProcessManager {}
class MockXcodeProjectInterpreter extends Mock implements XcodeProjectInterpreter {}
class MockPlatform extends Mock implements Platform {}
void main() {
group('Xcode', () {
MockProcessManager mockProcessManager;
Xcode xcode;
MockXcodeProjectInterpreter mockXcodeProjectInterpreter;
MockPlatform mockPlatform;
setUp(() {
mockProcessManager = MockProcessManager();
mockXcodeProjectInterpreter = MockXcodeProjectInterpreter();
xcode = Xcode();
mockPlatform = MockPlatform();
});
testUsingContext('xcodeSelectPath returns null when xcode-select is not installed', () {
@ -89,6 +93,80 @@ void main() {
XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
});
testUsingContext('isInstalledAndMeetsVersionCheck is false when not macOS', () {
when(mockPlatform.isMacOS).thenReturn(false);
expect(xcode.isInstalledAndMeetsVersionCheck, isFalse);
}, overrides: <Type, Generator>{
XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
Platform: () => mockPlatform,
});
testUsingContext('isInstalledAndMeetsVersionCheck is false when not installed', () {
when(mockPlatform.isMacOS).thenReturn(true);
const String xcodePath = '/Applications/Xcode8.0.app/Contents/Developer';
when(mockProcessManager.runSync(<String>['/usr/bin/xcode-select', '--print-path']))
.thenReturn(ProcessResult(1, 0, xcodePath, ''));
when(mockXcodeProjectInterpreter.isInstalled).thenReturn(false);
expect(xcode.isInstalledAndMeetsVersionCheck, isFalse);
}, overrides: <Type, Generator>{
XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
Platform: () => mockPlatform,
ProcessManager: () => mockProcessManager
});
testUsingContext('isInstalledAndMeetsVersionCheck is false when no xcode-select', () {
when(mockPlatform.isMacOS).thenReturn(true);
when(mockProcessManager.runSync(<String>['/usr/bin/xcode-select', '--print-path']))
.thenReturn(ProcessResult(1, 127, '', 'ERROR'));
when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true);
when(mockXcodeProjectInterpreter.majorVersion).thenReturn(9);
when(mockXcodeProjectInterpreter.minorVersion).thenReturn(1);
expect(xcode.isInstalledAndMeetsVersionCheck, isFalse);
}, overrides: <Type, Generator>{
XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
Platform: () => mockPlatform,
ProcessManager: () => mockProcessManager
});
testUsingContext('isInstalledAndMeetsVersionCheck is false when version not satisfied', () {
when(mockPlatform.isMacOS).thenReturn(true);
const String xcodePath = '/Applications/Xcode8.0.app/Contents/Developer';
when(mockProcessManager.runSync(<String>['/usr/bin/xcode-select', '--print-path']))
.thenReturn(ProcessResult(1, 0, xcodePath, ''));
when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true);
when(mockXcodeProjectInterpreter.majorVersion).thenReturn(8);
when(mockXcodeProjectInterpreter.minorVersion).thenReturn(0);
expect(xcode.isInstalledAndMeetsVersionCheck, isFalse);
}, overrides: <Type, Generator>{
XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
Platform: () => mockPlatform,
ProcessManager: () => mockProcessManager
});
testUsingContext('isInstalledAndMeetsVersionCheck is true when macOS and installed and version is satisfied', () {
when(mockPlatform.isMacOS).thenReturn(true);
const String xcodePath = '/Applications/Xcode8.0.app/Contents/Developer';
when(mockProcessManager.runSync(<String>['/usr/bin/xcode-select', '--print-path']))
.thenReturn(ProcessResult(1, 0, xcodePath, ''));
when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true);
when(mockXcodeProjectInterpreter.majorVersion).thenReturn(9);
when(mockXcodeProjectInterpreter.minorVersion).thenReturn(1);
expect(xcode.isInstalledAndMeetsVersionCheck, isTrue);
}, overrides: <Type, Generator>{
XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
Platform: () => mockPlatform,
ProcessManager: () => mockProcessManager
});
testUsingContext('eulaSigned is false when clang is not installed', () {
when(mockProcessManager.runSync(<String>['/usr/bin/xcrun', 'clang']))
.thenThrow(const ProcessException('/usr/bin/xcrun', <String>['clang']));

View file

@ -335,6 +335,10 @@ class FakeXcodeProjectInterpreter implements XcodeProjectInterpreter {
return <String, String>{};
}
@override
void cleanWorkspace(String workspacePath, String scheme) {
}
@override
Future<XcodeProjectInfo> getInfo(String projectPath) async {
return XcodeProjectInfo(