mirror of
https://github.com/flutter/flutter
synced 2024-09-17 23:31:55 +00:00
Clean Xcode workspace during flutter clean (#38992)
This commit is contained in:
parent
aa41088e76
commit
892d62f03a
|
@ -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()) {
|
||||
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');
|
||||
} else {
|
||||
printError('Failed to remove $path: $error');
|
||||
}
|
||||
throwToolExit(error.toString());
|
||||
}
|
||||
} finally {
|
||||
deletionStatus.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -555,10 +555,6 @@ abstract class FlutterCommand extends Command<void> {
|
|||
return deviceList.single;
|
||||
}
|
||||
|
||||
void printNoConnectedDevices() {
|
||||
printStatus(userMessages.flutterNoConnectedDevices);
|
||||
}
|
||||
|
||||
@protected
|
||||
@mustCallSuper
|
||||
Future<void> validateCommand() async {
|
||||
|
|
|
@ -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']);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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']));
|
||||
|
|
|
@ -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(
|
||||
|
|
Loading…
Reference in a new issue