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 {
plistParser: globals.plistParser,
processManager: globals.processManager,
),
...VsCodeValidator.installedValidators(globals.fs, globals.platform),
...VsCodeValidator.installedValidators(globals.fs, globals.platform, globals.processManager),
];
final ProxyValidator proxyValidator = ProxyValidator(platform: globals.platform);
_validators = <DoctorValidator>[
......
......@@ -2,7 +2,11 @@
// 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 'package:process/process.dart';
import '../base/file_system.dart';
import '../base/io.dart';
import '../base/platform.dart';
import '../base/utils.dart';
import '../base/version.dart';
......@@ -88,9 +92,10 @@ class VsCode {
static List<VsCode> allInstalled(
FileSystem fileSystem,
Platform platform,
ProcessManager processManager,
) {
if (platform.isMacOS) {
return _installedMacOS(fileSystem, platform);
return _installedMacOS(fileSystem, platform, processManager);
}
if (platform.isWindows) {
return _installedWindows(fileSystem, platform);
......@@ -110,15 +115,35 @@ class VsCode {
// macOS Extensions:
// $HOME/.vscode/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;
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'),
'.vscode',
),
if (homeDirPath != null)
_VsCodeInstallLocation(
VsCodeInstallLocation(
fileSystem.path.join(
homeDirPath,
'Applications',
......@@ -127,13 +152,13 @@ class VsCode {
),
'.vscode',
),
_VsCodeInstallLocation(
VsCodeInstallLocation(
fileSystem.path.join('/Applications', 'Visual Studio Code - Insiders.app', 'Contents'),
'.vscode-insiders',
isInsiders: true,
),
if (homeDirPath != null)
_VsCodeInstallLocation(
VsCodeInstallLocation(
fileSystem.path.join(
homeDirPath,
'Applications',
......@@ -143,7 +168,18 @@ class VsCode {
'.vscode-insiders',
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:
......@@ -166,20 +202,20 @@ class VsCode {
final String? progFiles = platform.environment['programfiles'];
final String? localAppData = platform.environment['localappdata'];
final List<_VsCodeInstallLocation> searchLocations = <_VsCodeInstallLocation>[
final List<VsCodeInstallLocation> searchLocations = <VsCodeInstallLocation>[
if (localAppData != null)
_VsCodeInstallLocation(
VsCodeInstallLocation(
fileSystem.path.join(localAppData, r'Programs\Microsoft VS Code'),
'.vscode',
),
if (progFiles86 != null)
...<_VsCodeInstallLocation>[
_VsCodeInstallLocation(
...<VsCodeInstallLocation>[
VsCodeInstallLocation(
fileSystem.path.join(progFiles86, 'Microsoft VS Code'),
'.vscode',
edition: '32-bit edition',
),
_VsCodeInstallLocation(
VsCodeInstallLocation(
fileSystem.path.join(progFiles86, 'Microsoft VS Code Insiders'),
'.vscode-insiders',
edition: '32-bit edition',
......@@ -187,13 +223,13 @@ class VsCode {
),
],
if (progFiles != null)
...<_VsCodeInstallLocation>[
_VsCodeInstallLocation(
...<VsCodeInstallLocation>[
VsCodeInstallLocation(
fileSystem.path.join(progFiles, 'Microsoft VS Code'),
'.vscode',
edition: '64-bit edition',
),
_VsCodeInstallLocation(
VsCodeInstallLocation(
fileSystem.path.join(progFiles, 'Microsoft VS Code Insiders'),
'.vscode-insiders',
edition: '64-bit edition',
......@@ -201,7 +237,7 @@ class VsCode {
),
],
if (localAppData != null)
_VsCodeInstallLocation(
VsCodeInstallLocation(
fileSystem.path.join(localAppData, r'Programs\Microsoft VS Code Insiders'),
'.vscode-insiders',
isInsiders: true,
......@@ -217,9 +253,9 @@ class VsCode {
// $HOME/.vscode/extensions
// $HOME/.vscode-insiders/extensions
static List<VsCode> _installedLinux(FileSystem fileSystem, Platform platform) {
return _findInstalled(<_VsCodeInstallLocation>[
const _VsCodeInstallLocation('/usr/share/code', '.vscode'),
const _VsCodeInstallLocation(
return _findInstalled(<VsCodeInstallLocation>[
const VsCodeInstallLocation('/usr/share/code', '.vscode'),
const VsCodeInstallLocation(
'/usr/share/code-insiders',
'.vscode-insiders',
isInsiders: true,
......@@ -228,18 +264,18 @@ class VsCode {
}
static List<VsCode> _findInstalled(
List<_VsCodeInstallLocation> allLocations,
Iterable<VsCodeInstallLocation> allLocations,
FileSystem fileSystem,
Platform platform,
) {
final Iterable<_VsCodeInstallLocation> searchLocations =
final Iterable<VsCodeInstallLocation> searchLocations =
_includeInsiders
? allLocations
: allLocations.where((_VsCodeInstallLocation p) => p.isInsiders != true);
: allLocations.where((VsCodeInstallLocation p) => p.isInsiders != true);
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;
if (homeDirPath != null && fileSystem.isDirectorySync(searchLocation.installPath)) {
final String extensionDirectory = fileSystem.path.join(
......@@ -280,8 +316,10 @@ class VsCode {
}
}
class _VsCodeInstallLocation {
const _VsCodeInstallLocation(
@immutable
@visibleForTesting
class VsCodeInstallLocation {
const VsCodeInstallLocation(
this.installPath,
this.extensionsFolder, {
this.edition,
......@@ -292,4 +330,17 @@ class _VsCodeInstallLocation {
final String extensionsFolder;
final String? edition;
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 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:process/process.dart';
import '../base/file_system.dart';
import '../base/platform.dart';
import '../base/user_messages.dart';
......@@ -14,9 +16,9 @@ class VsCodeValidator extends DoctorValidator {
final VsCode _vsCode;
static Iterable<DoctorValidator> installedValidators(FileSystem fileSystem, Platform platform) {
static Iterable<DoctorValidator> installedValidators(FileSystem fileSystem, Platform platform, ProcessManager processManager) {
return VsCode
.allInstalled(fileSystem, platform)
.allInstalled(fileSystem, platform, processManager)
.map<DoctorValidator>((VsCode vsCode) => VsCodeValidator(vsCode));
}
......
......@@ -3,12 +3,35 @@
// found in the LICENSE file.
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/vscode/vscode.dart';
import '../../src/common.dart';
import '../../src/fake_process_manager.dart';
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', () {
final MemoryFileSystem fileSystem = MemoryFileSystem.test();
// Create invalid JSON file.
......@@ -20,4 +43,51 @@ void main() {
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';
import 'package:flutter_tools/src/vscode/vscode.dart';
import '../../src/common.dart';
import '../../src/fake_process_manager.dart';
void main() {
testWithoutContext('VsCode search locations on windows supports an empty environment', () {
......@@ -17,6 +18,6 @@ void main() {
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