Unverified Commit 7aa188ba authored by Jenn Magder's avatar Jenn Magder Committed by GitHub

Find VS Code installations with Spotlight query on macOS (#80477)

parent 8d5f08fe
...@@ -86,7 +86,7 @@ class _DefaultDoctorValidatorsProvider implements DoctorValidatorsProvider { ...@@ -86,7 +86,7 @@ class _DefaultDoctorValidatorsProvider implements DoctorValidatorsProvider {
plistParser: globals.plistParser, plistParser: globals.plistParser,
processManager: globals.processManager, processManager: globals.processManager,
), ),
...VsCodeValidator.installedValidators(globals.fs, globals.platform), ...VsCodeValidator.installedValidators(globals.fs, globals.platform, globals.processManager),
]; ];
final ProxyValidator proxyValidator = ProxyValidator(platform: globals.platform); final ProxyValidator proxyValidator = ProxyValidator(platform: globals.platform);
_validators = <DoctorValidator>[ _validators = <DoctorValidator>[
......
...@@ -2,7 +2,11 @@ ...@@ -2,7 +2,11 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'package:meta/meta.dart';
import 'package:process/process.dart';
import '../base/file_system.dart'; import '../base/file_system.dart';
import '../base/io.dart';
import '../base/platform.dart'; import '../base/platform.dart';
import '../base/utils.dart'; import '../base/utils.dart';
import '../base/version.dart'; import '../base/version.dart';
...@@ -88,9 +92,10 @@ class VsCode { ...@@ -88,9 +92,10 @@ class VsCode {
static List<VsCode> allInstalled( static List<VsCode> allInstalled(
FileSystem fileSystem, FileSystem fileSystem,
Platform platform, Platform platform,
ProcessManager processManager,
) { ) {
if (platform.isMacOS) { if (platform.isMacOS) {
return _installedMacOS(fileSystem, platform); return _installedMacOS(fileSystem, platform, processManager);
} }
if (platform.isWindows) { if (platform.isWindows) {
return _installedWindows(fileSystem, platform); return _installedWindows(fileSystem, platform);
...@@ -110,15 +115,35 @@ class VsCode { ...@@ -110,15 +115,35 @@ class VsCode {
// macOS Extensions: // macOS Extensions:
// $HOME/.vscode/extensions // $HOME/.vscode/extensions
// $HOME/.vscode-insiders/extensions // $HOME/.vscode-insiders/extensions
static List<VsCode> _installedMacOS(FileSystem fileSystem, Platform platform) { static List<VsCode> _installedMacOS(FileSystem fileSystem, Platform platform, ProcessManager processManager) {
final String? homeDirPath = FileSystemUtils(fileSystem: fileSystem, platform: platform).homeDirPath; final String? homeDirPath = FileSystemUtils(fileSystem: fileSystem, platform: platform).homeDirPath;
return _findInstalled(<_VsCodeInstallLocation>[
_VsCodeInstallLocation( String vsCodeSpotlightResult = '';
String vsCodeInsiderSpotlightResult = '';
// Query Spotlight for unexpected installation locations.
try {
final ProcessResult vsCodeSpotlightQueryResult = processManager.runSync(<String>[
'mdfind',
'kMDItemCFBundleIdentifier="com.microsoft.VSCode"',
]);
vsCodeSpotlightResult = vsCodeSpotlightQueryResult.stdout as String;
final ProcessResult vsCodeInsidersSpotlightQueryResult = processManager.runSync(<String>[
'mdfind',
'kMDItemCFBundleIdentifier="com.microsoft.VSCodeInsiders"',
]);
vsCodeInsiderSpotlightResult = vsCodeInsidersSpotlightQueryResult.stdout as String;
} on ProcessException {
// The Spotlight query is a nice-to-have, continue checking known installation locations.
}
// De-duplicated set.
return _findInstalled(<VsCodeInstallLocation>{
VsCodeInstallLocation(
fileSystem.path.join('/Applications', 'Visual Studio Code.app', 'Contents'), fileSystem.path.join('/Applications', 'Visual Studio Code.app', 'Contents'),
'.vscode', '.vscode',
), ),
if (homeDirPath != null) if (homeDirPath != null)
_VsCodeInstallLocation( VsCodeInstallLocation(
fileSystem.path.join( fileSystem.path.join(
homeDirPath, homeDirPath,
'Applications', 'Applications',
...@@ -127,13 +152,13 @@ class VsCode { ...@@ -127,13 +152,13 @@ class VsCode {
), ),
'.vscode', '.vscode',
), ),
_VsCodeInstallLocation( VsCodeInstallLocation(
fileSystem.path.join('/Applications', 'Visual Studio Code - Insiders.app', 'Contents'), fileSystem.path.join('/Applications', 'Visual Studio Code - Insiders.app', 'Contents'),
'.vscode-insiders', '.vscode-insiders',
isInsiders: true, isInsiders: true,
), ),
if (homeDirPath != null) if (homeDirPath != null)
_VsCodeInstallLocation( VsCodeInstallLocation(
fileSystem.path.join( fileSystem.path.join(
homeDirPath, homeDirPath,
'Applications', 'Applications',
...@@ -143,7 +168,18 @@ class VsCode { ...@@ -143,7 +168,18 @@ class VsCode {
'.vscode-insiders', '.vscode-insiders',
isInsiders: true, isInsiders: true,
), ),
], fileSystem, platform); for (final String vsCodePath in LineSplitter.split(vsCodeSpotlightResult))
VsCodeInstallLocation(
fileSystem.path.join(vsCodePath, 'Contents'),
'.vscode',
),
for (final String vsCodeInsidersPath in LineSplitter.split(vsCodeInsiderSpotlightResult))
VsCodeInstallLocation(
fileSystem.path.join(vsCodeInsidersPath, 'Contents'),
'.vscode-insiders',
isInsiders: true,
),
}, fileSystem, platform);
} }
// Windows: // Windows:
...@@ -166,20 +202,20 @@ class VsCode { ...@@ -166,20 +202,20 @@ class VsCode {
final String? progFiles = platform.environment['programfiles']; final String? progFiles = platform.environment['programfiles'];
final String? localAppData = platform.environment['localappdata']; final String? localAppData = platform.environment['localappdata'];
final List<_VsCodeInstallLocation> searchLocations = <_VsCodeInstallLocation>[ final List<VsCodeInstallLocation> searchLocations = <VsCodeInstallLocation>[
if (localAppData != null) if (localAppData != null)
_VsCodeInstallLocation( VsCodeInstallLocation(
fileSystem.path.join(localAppData, r'Programs\Microsoft VS Code'), fileSystem.path.join(localAppData, r'Programs\Microsoft VS Code'),
'.vscode', '.vscode',
), ),
if (progFiles86 != null) if (progFiles86 != null)
...<_VsCodeInstallLocation>[ ...<VsCodeInstallLocation>[
_VsCodeInstallLocation( VsCodeInstallLocation(
fileSystem.path.join(progFiles86, 'Microsoft VS Code'), fileSystem.path.join(progFiles86, 'Microsoft VS Code'),
'.vscode', '.vscode',
edition: '32-bit edition', edition: '32-bit edition',
), ),
_VsCodeInstallLocation( VsCodeInstallLocation(
fileSystem.path.join(progFiles86, 'Microsoft VS Code Insiders'), fileSystem.path.join(progFiles86, 'Microsoft VS Code Insiders'),
'.vscode-insiders', '.vscode-insiders',
edition: '32-bit edition', edition: '32-bit edition',
...@@ -187,13 +223,13 @@ class VsCode { ...@@ -187,13 +223,13 @@ class VsCode {
), ),
], ],
if (progFiles != null) if (progFiles != null)
...<_VsCodeInstallLocation>[ ...<VsCodeInstallLocation>[
_VsCodeInstallLocation( VsCodeInstallLocation(
fileSystem.path.join(progFiles, 'Microsoft VS Code'), fileSystem.path.join(progFiles, 'Microsoft VS Code'),
'.vscode', '.vscode',
edition: '64-bit edition', edition: '64-bit edition',
), ),
_VsCodeInstallLocation( VsCodeInstallLocation(
fileSystem.path.join(progFiles, 'Microsoft VS Code Insiders'), fileSystem.path.join(progFiles, 'Microsoft VS Code Insiders'),
'.vscode-insiders', '.vscode-insiders',
edition: '64-bit edition', edition: '64-bit edition',
...@@ -201,7 +237,7 @@ class VsCode { ...@@ -201,7 +237,7 @@ class VsCode {
), ),
], ],
if (localAppData != null) if (localAppData != null)
_VsCodeInstallLocation( VsCodeInstallLocation(
fileSystem.path.join(localAppData, r'Programs\Microsoft VS Code Insiders'), fileSystem.path.join(localAppData, r'Programs\Microsoft VS Code Insiders'),
'.vscode-insiders', '.vscode-insiders',
isInsiders: true, isInsiders: true,
...@@ -217,9 +253,9 @@ class VsCode { ...@@ -217,9 +253,9 @@ class VsCode {
// $HOME/.vscode/extensions // $HOME/.vscode/extensions
// $HOME/.vscode-insiders/extensions // $HOME/.vscode-insiders/extensions
static List<VsCode> _installedLinux(FileSystem fileSystem, Platform platform) { static List<VsCode> _installedLinux(FileSystem fileSystem, Platform platform) {
return _findInstalled(<_VsCodeInstallLocation>[ return _findInstalled(<VsCodeInstallLocation>[
const _VsCodeInstallLocation('/usr/share/code', '.vscode'), const VsCodeInstallLocation('/usr/share/code', '.vscode'),
const _VsCodeInstallLocation( const VsCodeInstallLocation(
'/usr/share/code-insiders', '/usr/share/code-insiders',
'.vscode-insiders', '.vscode-insiders',
isInsiders: true, isInsiders: true,
...@@ -228,18 +264,18 @@ class VsCode { ...@@ -228,18 +264,18 @@ class VsCode {
} }
static List<VsCode> _findInstalled( static List<VsCode> _findInstalled(
List<_VsCodeInstallLocation> allLocations, Iterable<VsCodeInstallLocation> allLocations,
FileSystem fileSystem, FileSystem fileSystem,
Platform platform, Platform platform,
) { ) {
final Iterable<_VsCodeInstallLocation> searchLocations = final Iterable<VsCodeInstallLocation> searchLocations =
_includeInsiders _includeInsiders
? allLocations ? allLocations
: allLocations.where((_VsCodeInstallLocation p) => p.isInsiders != true); : allLocations.where((VsCodeInstallLocation p) => p.isInsiders != true);
final List<VsCode> results = <VsCode>[]; final List<VsCode> results = <VsCode>[];
for (final _VsCodeInstallLocation searchLocation in searchLocations) { for (final VsCodeInstallLocation searchLocation in searchLocations) {
final String? homeDirPath = FileSystemUtils(fileSystem: fileSystem, platform: platform).homeDirPath; final String? homeDirPath = FileSystemUtils(fileSystem: fileSystem, platform: platform).homeDirPath;
if (homeDirPath != null && fileSystem.isDirectorySync(searchLocation.installPath)) { if (homeDirPath != null && fileSystem.isDirectorySync(searchLocation.installPath)) {
final String extensionDirectory = fileSystem.path.join( final String extensionDirectory = fileSystem.path.join(
...@@ -280,8 +316,10 @@ class VsCode { ...@@ -280,8 +316,10 @@ class VsCode {
} }
} }
class _VsCodeInstallLocation { @immutable
const _VsCodeInstallLocation( @visibleForTesting
class VsCodeInstallLocation {
const VsCodeInstallLocation(
this.installPath, this.installPath,
this.extensionsFolder, { this.extensionsFolder, {
this.edition, this.edition,
...@@ -292,4 +330,17 @@ class _VsCodeInstallLocation { ...@@ -292,4 +330,17 @@ class _VsCodeInstallLocation {
final String extensionsFolder; final String extensionsFolder;
final String? edition; final String? edition;
final bool isInsiders; final bool isInsiders;
@override
bool operator ==(Object other) {
return other is VsCodeInstallLocation &&
other.installPath == installPath &&
other.extensionsFolder == extensionsFolder &&
other.edition == edition &&
other.isInsiders == isInsiders;
}
@override
// Lowest bit is for isInsiders boolean.
int get hashCode => (installPath.hashCode ^ extensionsFolder.hashCode ^ edition.hashCode) << 1 | (isInsiders ? 1 : 0);
} }
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'package:process/process.dart';
import '../base/file_system.dart'; import '../base/file_system.dart';
import '../base/platform.dart'; import '../base/platform.dart';
import '../base/user_messages.dart'; import '../base/user_messages.dart';
...@@ -14,9 +16,9 @@ class VsCodeValidator extends DoctorValidator { ...@@ -14,9 +16,9 @@ class VsCodeValidator extends DoctorValidator {
final VsCode _vsCode; final VsCode _vsCode;
static Iterable<DoctorValidator> installedValidators(FileSystem fileSystem, Platform platform) { static Iterable<DoctorValidator> installedValidators(FileSystem fileSystem, Platform platform, ProcessManager processManager) {
return VsCode return VsCode
.allInstalled(fileSystem, platform) .allInstalled(fileSystem, platform, processManager)
.map<DoctorValidator>((VsCode vsCode) => VsCodeValidator(vsCode)); .map<DoctorValidator>((VsCode vsCode) => VsCodeValidator(vsCode));
} }
......
...@@ -3,12 +3,35 @@ ...@@ -3,12 +3,35 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'package:file/memory.dart'; import 'package:file/memory.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/base/version.dart'; import 'package:flutter_tools/src/base/version.dart';
import 'package:flutter_tools/src/vscode/vscode.dart'; import 'package:flutter_tools/src/vscode/vscode.dart';
import '../../src/common.dart'; import '../../src/common.dart';
import '../../src/fake_process_manager.dart';
void main() { void main() {
testWithoutContext('VsCodeInstallLocation equality', () {
const VsCodeInstallLocation installLocation1 = VsCodeInstallLocation('abc', 'zyx', edition: '123', isInsiders: true);
const VsCodeInstallLocation installLocation2 = VsCodeInstallLocation('abc', 'zyx', edition: '123', isInsiders: true);
const VsCodeInstallLocation installLocation3 = VsCodeInstallLocation('cba', 'zyx', edition: '123', isInsiders: true);
const VsCodeInstallLocation installLocation4 = VsCodeInstallLocation('abc', 'xyz', edition: '123', isInsiders: true);
const VsCodeInstallLocation installLocation5 = VsCodeInstallLocation('abc', 'xyz', edition: '321', isInsiders: true);
const VsCodeInstallLocation installLocation6 = VsCodeInstallLocation('abc', 'zyx', edition: '123', isInsiders: false);
expect(installLocation1, installLocation2);
expect(installLocation1.hashCode, installLocation2.hashCode);
expect(installLocation1, isNot(installLocation3));
expect(installLocation1.hashCode, isNot(installLocation3.hashCode));
expect(installLocation1, isNot(installLocation4));
expect(installLocation1.hashCode, isNot(installLocation4.hashCode));
expect(installLocation1, isNot(installLocation5));
expect(installLocation1.hashCode, isNot(installLocation5.hashCode));
expect(installLocation1, isNot(installLocation6));
expect(installLocation1.hashCode, isNot(installLocation6.hashCode));
});
testWithoutContext('VsCode.fromDirectory does not crash when packages.json is malformed', () { testWithoutContext('VsCode.fromDirectory does not crash when packages.json is malformed', () {
final MemoryFileSystem fileSystem = MemoryFileSystem.test(); final MemoryFileSystem fileSystem = MemoryFileSystem.test();
// Create invalid JSON file. // Create invalid JSON file.
...@@ -20,4 +43,51 @@ void main() { ...@@ -20,4 +43,51 @@ void main() {
expect(vsCode.version, Version.unknown); expect(vsCode.version, Version.unknown);
}); });
testWithoutContext('can locate non-Insider installations on macOS', () {
final FileSystem fileSystem = MemoryFileSystem.test();
const String home = '/home/me';
final Platform platform = FakePlatform(operatingSystem: 'macos', environment: <String, String>{'HOME': home});
final String randomLocation = fileSystem.path.join(
'/',
'random',
'Visual Studio Code.app',
);
fileSystem.directory(fileSystem.path.join(randomLocation, 'Contents')).createSync(recursive: true);
final String randomInsidersLocation = fileSystem.path.join(
'/',
'random',
'Visual Studio Code - Insiders.app',
);
fileSystem.directory(fileSystem.path.join(randomInsidersLocation, 'Contents')).createSync(recursive: true);
fileSystem.directory(fileSystem.path.join('/', 'Applications', 'Visual Studio Code.app', 'Contents')).createSync(recursive: true);
fileSystem.directory(fileSystem.path.join('/', 'Applications', 'Visual Studio Code - Insiders.app', 'Contents')).createSync(recursive: true);
fileSystem.directory(fileSystem.path.join(home, 'Applications', 'Visual Studio Code.app', 'Contents')).createSync(recursive: true);
fileSystem.directory(fileSystem.path.join(home, 'Applications', 'Visual Studio Code - Insiders.app', 'Contents')).createSync(recursive: true);
final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
FakeCommand(
command: const <String>[
'mdfind',
'kMDItemCFBundleIdentifier="com.microsoft.VSCode"',
],
stdout: randomLocation,
),
FakeCommand(
command: const <String>[
'mdfind',
'kMDItemCFBundleIdentifier="com.microsoft.VSCodeInsiders"',
],
stdout: randomInsidersLocation,
),
]);
final List<VsCode> installed = VsCode.allInstalled(fileSystem, platform, processManager);
// Finds three non-Insider installations.
expect(installed.length, 3);
expect(processManager, hasNoRemainingExpectations);
});
} }
...@@ -8,6 +8,7 @@ import 'package:flutter_tools/src/base/platform.dart'; ...@@ -8,6 +8,7 @@ import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/vscode/vscode.dart'; import 'package:flutter_tools/src/vscode/vscode.dart';
import '../../src/common.dart'; import '../../src/common.dart';
import '../../src/fake_process_manager.dart';
void main() { void main() {
testWithoutContext('VsCode search locations on windows supports an empty environment', () { testWithoutContext('VsCode search locations on windows supports an empty environment', () {
...@@ -17,6 +18,6 @@ void main() { ...@@ -17,6 +18,6 @@ void main() {
environment: <String, String>{}, environment: <String, String>{},
); );
expect(VsCode.allInstalled(fileSystem, platform), isEmpty); expect(VsCode.allInstalled(fileSystem, platform, FakeProcessManager.any()), isEmpty);
}); });
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment