Unverified Commit ba4177f6 authored by Jenn Magder's avatar Jenn Magder Committed by GitHub

Find Android Studio installations with Spotlight query on macOS (#80475)

parent 0021a08c
...@@ -7,6 +7,7 @@ import '../base/io.dart'; ...@@ -7,6 +7,7 @@ import '../base/io.dart';
import '../base/process.dart'; import '../base/process.dart';
import '../base/utils.dart'; import '../base/utils.dart';
import '../base/version.dart'; import '../base/version.dart';
import '../convert.dart';
import '../globals_null_migrated.dart' as globals; import '../globals_null_migrated.dart' as globals;
import '../ios/plist_parser.dart'; import '../ios/plist_parser.dart';
...@@ -44,7 +45,7 @@ class AndroidStudio implements Comparable<AndroidStudio> { ...@@ -44,7 +45,7 @@ class AndroidStudio implements Comparable<AndroidStudio> {
Map<String, dynamic> plistValues = globals.plistParser.parseFile(plistFile); Map<String, dynamic> plistValues = globals.plistParser.parseFile(plistFile);
// As AndroidStudio managed by JetBrainsToolbox could have a wrapper pointing to the real Android Studio. // As AndroidStudio managed by JetBrainsToolbox could have a wrapper pointing to the real Android Studio.
// Check if we've found a JetBrainsToolbox wrapper and deal with it properly. // Check if we've found a JetBrainsToolbox wrapper and deal with it properly.
final String jetBrainsToolboxAppBundlePath = plistValues['JetBrainsToolboxApp'] as String; final String? jetBrainsToolboxAppBundlePath = plistValues['JetBrainsToolboxApp'] as String?;
if (jetBrainsToolboxAppBundlePath != null) { if (jetBrainsToolboxAppBundlePath != null) {
studioPath = globals.fs.path.join(jetBrainsToolboxAppBundlePath, 'Contents'); studioPath = globals.fs.path.join(jetBrainsToolboxAppBundlePath, 'Contents');
plistFile = globals.fs.path.join(studioPath, 'Info.plist'); plistFile = globals.fs.path.join(studioPath, 'Info.plist');
...@@ -295,9 +296,28 @@ class AndroidStudio implements Comparable<AndroidStudio> { ...@@ -295,9 +296,28 @@ class AndroidStudio implements Comparable<AndroidStudio> {
} }
} }
// Query Spotlight for unexpected installation locations.
String spotlightQueryResult = '';
try {
final ProcessResult spotlightResult = globals.processManager.runSync(<String>[
'mdfind',
// com.google.android.studio, com.google.android.studio-EAP
'kMDItemCFBundleIdentifier="com.google.android.studio*"',
]);
spotlightQueryResult = spotlightResult.stdout as String;
} on ProcessException {
// The Spotlight query is a nice-to-have, continue checking known installation locations.
}
for (final String studioPath in LineSplitter.split(spotlightQueryResult)) {
final Directory appBundle = globals.fs.directory(studioPath);
if (!candidatePaths.any((FileSystemEntity e) => e.path == studioPath)) {
candidatePaths.add(appBundle);
}
}
return candidatePaths return candidatePaths
.map<AndroidStudio>((FileSystemEntity e) => AndroidStudio.fromMacOSBundle(e.path)) .map<AndroidStudio>((FileSystemEntity e) => AndroidStudio.fromMacOSBundle(e.path))
.where((AndroidStudio s) => s != null) .whereType<AndroidStudio>()
.toList(); .toList();
} }
......
...@@ -143,6 +143,11 @@ void main() { ...@@ -143,6 +143,11 @@ void main() {
workingDirectory: anyNamed('workingDirectory'), workingDirectory: anyNamed('workingDirectory'),
environment: anyNamed('environment'))) environment: anyNamed('environment')))
.thenAnswer((_) => Future<ProcessResult>.value(ProcessResult(0, 0, 'assembleRelease', ''))); .thenAnswer((_) => Future<ProcessResult>.value(ProcessResult(0, 0, 'assembleRelease', '')));
when(mockProcessManager.runSync(<String>['mdfind', 'kMDItemCFBundleIdentifier="com.google.android.studio*"'],
workingDirectory: anyNamed('workingDirectory'),
environment: anyNamed('environment')))
.thenReturn(ProcessResult(0, 0, '', ''));
// Fallback with error. // Fallback with error.
final Process process = createMockProcess(exitCode: 1); final Process process = createMockProcess(exitCode: 1);
when(mockProcessManager.start(any, when(mockProcessManager.start(any,
......
...@@ -15,6 +15,7 @@ import 'package:test/fake.dart'; ...@@ -15,6 +15,7 @@ import 'package:test/fake.dart';
import '../../src/common.dart'; import '../../src/common.dart';
import '../../src/context.dart'; import '../../src/context.dart';
import '../../src/fake_process_manager.dart';
const String homeLinux = '/home/me'; const String homeLinux = '/home/me';
const String homeMac = '/Users/me'; const String homeMac = '/Users/me';
...@@ -99,6 +100,7 @@ void main() { ...@@ -99,6 +100,7 @@ void main() {
FileSystemUtils fsUtils; FileSystemUtils fsUtils;
Platform platform; Platform platform;
FakePlistUtils plistUtils; FakePlistUtils plistUtils;
FakeProcessManager processManager;
setUp(() { setUp(() {
plistUtils = FakePlistUtils(); plistUtils = FakePlistUtils();
...@@ -107,6 +109,7 @@ void main() { ...@@ -107,6 +109,7 @@ void main() {
fileSystem: fileSystem, fileSystem: fileSystem,
platform: platform, platform: platform,
); );
processManager = FakeProcessManager.empty();
}); });
testUsingContext('Can discover Android Studio >=4.1 location on Mac', () { testUsingContext('Can discover Android Studio >=4.1 location on Mac', () {
...@@ -174,6 +177,91 @@ void main() { ...@@ -174,6 +177,91 @@ void main() {
PlistParser: () => plistUtils, PlistParser: () => plistUtils,
}); });
testUsingContext('Can discover installation from Spotlight query', () {
// One in expected location.
final String studioInApplication = fileSystem.path.join(
'/',
'Application',
'Android Studio.app',
);
final String studioInApplicationPlistFolder = fileSystem.path.join(
studioInApplication,
'Contents',
);
fileSystem.directory(studioInApplicationPlistFolder).createSync(recursive: true);
final String plistFilePath = fileSystem.path.join(studioInApplicationPlistFolder, 'Info.plist');
plistUtils.fileContents[plistFilePath] = macStudioInfoPlist4_1;
// Two in random location only Spotlight knows about.
final String randomLocation1 = fileSystem.path.join(
'/',
'random',
'Android Studio Preview.app',
);
final String randomLocation1PlistFolder = fileSystem.path.join(
randomLocation1,
'Contents',
);
fileSystem.directory(randomLocation1PlistFolder).createSync(recursive: true);
final String randomLocation1PlistPath = fileSystem.path.join(randomLocation1PlistFolder, 'Info.plist');
plistUtils.fileContents[randomLocation1PlistPath] = macStudioInfoPlist4_1;
final String randomLocation2 = fileSystem.path.join(
'/',
'random',
'Android Studio with Blaze.app',
);
final String randomLocation2PlistFolder = fileSystem.path.join(
randomLocation2,
'Contents',
);
fileSystem.directory(randomLocation2PlistFolder).createSync(recursive: true);
final String randomLocation2PlistPath = fileSystem.path.join(randomLocation2PlistFolder, 'Info.plist');
plistUtils.fileContents[randomLocation2PlistPath] = macStudioInfoPlist4_1;
final String javaBin = fileSystem.path.join('jre', 'jdk', 'Contents', 'Home', 'bin', 'java');
// Spotlight finds the one known and two random installations.
processManager.addCommands(<FakeCommand>[
FakeCommand(
command: const <String>[
'mdfind',
'kMDItemCFBundleIdentifier="com.google.android.studio*"',
],
stdout: '$randomLocation1\n$randomLocation2\n$studioInApplication',
),
FakeCommand(
command: <String>[
fileSystem.path.join(randomLocation1, 'Contents', javaBin),
'-version',
],
),
FakeCommand(
command: <String>[
fileSystem.path.join(randomLocation2, 'Contents', javaBin),
'-version',
],
),
FakeCommand(
command: <String>[
fileSystem.path.join(studioInApplicationPlistFolder, javaBin),
'-version',
],
),
]);
// Results are de-duplicated, only 3 installed.
expect(AndroidStudio.allInstalled().length, 3);
expect(processManager, hasNoRemainingExpectations);
}, overrides: <Type, Generator>{
FileSystem: () => fileSystem,
FileSystemUtils: () => fsUtils,
ProcessManager: () => processManager,
// Custom home paths are not supported on macOS nor Windows yet,
// so we force the platform to fake Linux here.
Platform: () => platform,
PlistParser: () => plistUtils,
});
testUsingContext('finds latest valid install', () { testUsingContext('finds latest valid install', () {
final String applicationPlistFolder = globals.fs.path.join( final String applicationPlistFolder = globals.fs.path.join(
'/', '/',
......
...@@ -309,19 +309,17 @@ include ':app' ...@@ -309,19 +309,17 @@ include ':app'
group('Gradle local.properties', () { group('Gradle local.properties', () {
Artifacts localEngineArtifacts; Artifacts localEngineArtifacts;
FakePlatform android;
FileSystem fs; FileSystem fs;
setUp(() { setUp(() {
fs = MemoryFileSystem.test(); fs = MemoryFileSystem.test();
localEngineArtifacts = Artifacts.test(localEngine: 'out/android_arm'); localEngineArtifacts = Artifacts.test(localEngine: 'out/android_arm');
android = fakePlatform('android');
}); });
void testUsingAndroidContext(String description, dynamic Function() testMethod) { void testUsingAndroidContext(String description, dynamic Function() testMethod) {
testUsingContext(description, testMethod, overrides: <Type, Generator>{ testUsingContext(description, testMethod, overrides: <Type, Generator>{
Artifacts: () => localEngineArtifacts, Artifacts: () => localEngineArtifacts,
Platform: () => android, Platform: () => FakePlatform(operatingSystem: 'linux'),
FileSystem: () => fs, FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(), ProcessManager: () => FakeProcessManager.any(),
}); });
...@@ -626,12 +624,15 @@ flutter: ...@@ -626,12 +624,15 @@ flutter:
FakeAndroidSdk androidSdk; FakeAndroidSdk androidSdk;
AndroidGradleBuilder builder; AndroidGradleBuilder builder;
BufferLogger logger; BufferLogger logger;
Platform platform;
setUp(() { setUp(() {
logger = BufferLogger.test(); logger = BufferLogger.test();
fs = MemoryFileSystem.test(); fs = MemoryFileSystem.test();
fakeProcessManager = FakeProcessManager.empty(); fakeProcessManager = FakeProcessManager.empty();
androidSdk = FakeAndroidSdk(); androidSdk = FakeAndroidSdk();
platform = FakePlatform(operatingSystem: 'linux');
builder = AndroidGradleBuilder( builder = AndroidGradleBuilder(
logger: logger, logger: logger,
processManager: fakeProcessManager, processManager: fakeProcessManager,
...@@ -639,7 +640,7 @@ flutter: ...@@ -639,7 +640,7 @@ flutter:
artifacts: Artifacts.test(), artifacts: Artifacts.test(),
usage: TestUsage(), usage: TestUsage(),
gradleUtils: FakeGradleUtils(), gradleUtils: FakeGradleUtils(),
platform: FakePlatform(), platform: platform,
); );
}); });
...@@ -704,8 +705,8 @@ plugin2=${plugin2.path} ...@@ -704,8 +705,8 @@ plugin2=${plugin2.path}
'aar_init_script.gradle', 'aar_init_script.gradle',
); );
fakeProcessManager fakeProcessManager.addCommands(<FakeCommand>[
..addCommand(FakeCommand( FakeCommand(
command: <String>[ command: <String>[
'gradlew', 'gradlew',
'-I=$initScript', '-I=$initScript',
...@@ -721,8 +722,8 @@ plugin2=${plugin2.path} ...@@ -721,8 +722,8 @@ plugin2=${plugin2.path}
'assembleAarRelease', 'assembleAarRelease',
], ],
workingDirectory: plugin1.childDirectory('android').path, workingDirectory: plugin1.childDirectory('android').path,
)) ),
..addCommand(FakeCommand( FakeCommand(
command: <String>[ command: <String>[
'gradlew', 'gradlew',
'-I=$initScript', '-I=$initScript',
...@@ -738,7 +739,7 @@ plugin2=${plugin2.path} ...@@ -738,7 +739,7 @@ plugin2=${plugin2.path}
'assembleAarRelease', 'assembleAarRelease',
], ],
workingDirectory: plugin2.childDirectory('android').path, workingDirectory: plugin2.childDirectory('android').path,
)); )]);
await builder.buildPluginsAsAar( await builder.buildPluginsAsAar(
FlutterProject.fromDirectoryTest(androidDirectory), FlutterProject.fromDirectoryTest(androidDirectory),
...@@ -755,6 +756,7 @@ plugin2=${plugin2.path} ...@@ -755,6 +756,7 @@ plugin2=${plugin2.path}
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
AndroidSdk: () => androidSdk, AndroidSdk: () => androidSdk,
FileSystem: () => fs, FileSystem: () => fs,
Platform: () => platform,
ProcessManager: () => fakeProcessManager, ProcessManager: () => fakeProcessManager,
GradleUtils: () => FakeGradleUtils(), GradleUtils: () => FakeGradleUtils(),
}); });
...@@ -1008,14 +1010,6 @@ plugin1=${plugin1.path} ...@@ -1008,14 +1010,6 @@ plugin1=${plugin1.path}
}, skip: true); // TODO(jonahwilliams): This is an integration test and should be moved to the integration shard. }, skip: true); // TODO(jonahwilliams): This is an integration test and should be moved to the integration shard.
} }
FakePlatform fakePlatform(String name) {
return FakePlatform(
environment: <String, String>{'HOME': '/path/to/home'},
operatingSystem: name,
stdoutSupportsAnsi: false,
);
}
class FakeGradleUtils extends GradleUtils { class FakeGradleUtils extends GradleUtils {
@override @override
String getExecutable(FlutterProject project) { String getExecutable(FlutterProject project) {
......
...@@ -884,6 +884,7 @@ void main() { ...@@ -884,6 +884,7 @@ void main() {
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
Cache: () => cache, Cache: () => cache,
FileSystem: () => memoryFileSystem, FileSystem: () => memoryFileSystem,
Platform: () => FakePlatform(operatingSystem: 'linux'),
ProcessManager: () => FakeProcessManager.list(<FakeCommand>[ ProcessManager: () => FakeProcessManager.list(<FakeCommand>[
const FakeCommand(command: <String>[ const FakeCommand(command: <String>[
'/cache/bin/cache/flutter_gradle_wrapper.rand0/gradlew', '/cache/bin/cache/flutter_gradle_wrapper.rand0/gradlew',
......
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