From 77efc38b677a1f67d8db4498070cc2759d4aaa22 Mon Sep 17 00:00:00 2001 From: Jakob Andersen Date: Mon, 20 Feb 2017 11:02:50 +0100 Subject: [PATCH] Teach flutter tools to find gradle (#8241) * Teach flutter tools to find gradle Flutter tools will now use Gradle from Android Studio, which is now found automatically. flutter doctor will verify that Android Studio has been installed, and that the included Gradle is at least version 2.14.1. It is still possible to manually configure the path to Android Studio (flutter config --android-studio-dir=XXX) or Gradle (flutter config --gradle-dir=XXX), but this should only be necessary if they're installed somewhere non-standard. Only tested on Linux and macOS for now. Fixes #8131 --- packages/flutter_tools/lib/executable.dart | 2 +- .../lib/src/android/android_studio.dart | 257 ++++++++++++++++++ .../src/android/android_studio_validator.dart | 136 +++++++++ .../lib/src/android/android_workflow.dart | 6 +- .../flutter_tools/lib/src/android/gradle.dart | 119 ++------ packages/flutter_tools/lib/src/doctor.dart | 69 +++-- .../lib/src/ios/plist_utils.dart | 1 + packages/flutter_tools/test/src/context.dart | 2 +- 8 files changed, 455 insertions(+), 137 deletions(-) create mode 100644 packages/flutter_tools/lib/src/android/android_studio.dart create mode 100644 packages/flutter_tools/lib/src/android/android_studio_validator.dart diff --git a/packages/flutter_tools/lib/executable.dart b/packages/flutter_tools/lib/executable.dart index af22636709..d759f9ffe6 100644 --- a/packages/flutter_tools/lib/executable.dart +++ b/packages/flutter_tools/lib/executable.dart @@ -106,6 +106,7 @@ Future main(List args) async { context.putIfAbsent(FileSystem, () => new LocalFileSystem()); context.putIfAbsent(ProcessManager, () => new LocalProcessManager()); context.putIfAbsent(Logger, () => platform.isWindows ? new WindowsStdoutLogger() : new StdoutLogger()); + context.putIfAbsent(Config, () => new Config()); // Order-independent context entries context.putIfAbsent(DeviceManager, () => new DeviceManager()); @@ -114,7 +115,6 @@ Future main(List args) async { context.putIfAbsent(HotRunnerConfig, () => new HotRunnerConfig()); context.putIfAbsent(Cache, () => new Cache()); context.putIfAbsent(Artifacts, () => new CachedArtifacts()); - context.putIfAbsent(Config, () => new Config()); context.putIfAbsent(OperatingSystemUtils, () => new OperatingSystemUtils()); context.putIfAbsent(Xcode, () => new Xcode()); context.putIfAbsent(IOSSimulatorUtils, () => new IOSSimulatorUtils()); diff --git a/packages/flutter_tools/lib/src/android/android_studio.dart b/packages/flutter_tools/lib/src/android/android_studio.dart new file mode 100644 index 0000000000..54f1c3298b --- /dev/null +++ b/packages/flutter_tools/lib/src/android/android_studio.dart @@ -0,0 +1,257 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:pub_semver/pub_semver.dart'; + +import '../base/common.dart'; +import '../base/context.dart'; +import '../base/file_system.dart'; +import '../base/os.dart'; +import '../base/platform.dart'; +import '../base/process_manager.dart'; +import '../globals.dart'; +import '../ios/plist_utils.dart'; + +AndroidStudio get androidStudio => + context.putIfAbsent(AndroidStudio, AndroidStudio.latestValid); + +// Android Studio layout: + +// Linux/Windows: +// $HOME/.AndroidStudioX.Y/system/.home + +// macOS: +// /Applications/Android Studio.app/Contents/ +// $HOME/Applications/Android Studio.app/Contents/ + +// $STUDIO_HOME/gradle/gradle-X.Y.Z/bin/gradle + +final Version minGradleVersion = new Version(2, 14, 1); + +/// Locate Gradle. +String get gradleExecutable { + // See if the user has explicitly configured gradle-dir. + String gradleDir = config.getValue('gradle-dir'); + if (gradleDir != null) { + if (fs.isFileSync(gradleDir)) + return gradleDir; + return fs.path.join(gradleDir, 'bin', 'gradle'); + } + return androidStudio?.gradleExecutable ?? os.which('gradle')?.path; +} + +class AndroidStudio implements Comparable { + AndroidStudio(this.directory, {this.version = '0.0', this.configured}) { + _init(); + } + + final String directory; + final String version; + final String configured; + + String _gradlePath; + bool _isValid = false; + List _validationMessages = []; + + factory AndroidStudio.fromMacOSBundle(String bundlePath) { + String studioPath = fs.path.join(bundlePath, 'Contents'); + String plistFile = fs.path.join(studioPath, 'Info.plist'); + String version = + getValueFromFile(plistFile, kCFBundleShortVersionStringKey); + return new AndroidStudio(studioPath, version: version); + } + + factory AndroidStudio.fromHomeDot(Directory homeDotDir) { + String version = homeDotDir.basename.substring('.AndroidStudio'.length); + String installPath; + try { + installPath = fs + .file(fs.path.join(homeDotDir.path, 'system', '.home')) + .readAsStringSync(); + } catch (e) { + // ignored + } + if (installPath != null && fs.isDirectorySync(installPath)) { + return new AndroidStudio(installPath, version: version); + } + return null; + } + + String get gradlePath => _gradlePath; + + String get gradleExecutable => fs.path + .join(_gradlePath, 'bin', platform.isWindows ? 'gradle.bat' : 'gradle'); + + bool get isValid => _isValid; + + List get validationMessages => _validationMessages; + + @override + int compareTo(AndroidStudio other) { + int result = version.compareTo(other.version); + if (result == 0) + return directory.compareTo(other.directory); + return result; + } + + /// Locates the newest, valid version of Android Studio. + static AndroidStudio latestValid() { + String configuredStudio = config.getValue('android-studio-dir'); + if (configuredStudio != null) { + String configuredStudioPath = configuredStudio; + if (os.isMacOS && !configuredStudioPath.endsWith('Contents')) + configuredStudioPath = fs.path.join(configuredStudioPath, 'Contents'); + return new AndroidStudio(configuredStudioPath, + configured: configuredStudio); + } + + // Find all available Studio installations. + List studios = allInstalled(); + if (studios.isEmpty) { + return null; + } + studios.sort(); + return studios.lastWhere((AndroidStudio s) => s.isValid, + orElse: () => null); + } + + static List allInstalled() => + platform.isMacOS ? _allMacOS() : _allLinuxOrWindows(); + + static List _allMacOS() { + List candidatePaths = []; + + void _checkForStudio(String path) { + List directories = fs + .directory(path) + .listSync() + .where((FileSystemEntity e) => e is Directory); + for (Directory directory in directories) { + if (directory.basename == 'Android Studio.app') { + candidatePaths.add(directory); + } else if (!directory.path.endsWith('.app')) { + _checkForStudio(directory.path); + } + } + } + + _checkForStudio('/Applications'); + _checkForStudio(fs.path.join(homeDirPath, 'Applications')); + + String configuredStudioDir = config.getValue('android-studio-dir'); + if (configuredStudioDir != null) { + FileSystemEntity configuredStudio = fs.file(configuredStudioDir); + if (configuredStudio.basename == 'Contents') { + configuredStudio = configuredStudio.parent; + } + if (!candidatePaths + .any((FileSystemEntity e) => e.path == configuredStudio.path)) { + candidatePaths.add(configuredStudio); + } + } + + return candidatePaths + .map((FileSystemEntity e) => new AndroidStudio.fromMacOSBundle(e.path)) + .where((AndroidStudio s) => s != null) + .toList(); + } + + static List _allLinuxOrWindows() { + List studios = []; + + bool _hasStudioAt(String path, {String newerThan}) { + return studios.any((AndroidStudio studio) { + if (studio.directory != path) return false; + if (newerThan != null) { + return studio.version.compareTo(newerThan) >= 0; + } + return true; + }); + } + + // Read all $HOME/AndroidStudio*/system/.home files. There may be several + // pointing to the same installation, so we grab only the latest one. + for (FileSystemEntity entity in fs.directory(homeDirPath).listSync()) { + if (entity is Directory && entity.basename.startsWith('.AndroidStudio')) { + AndroidStudio studio = new AndroidStudio.fromHomeDot(entity); + if (studio != null && + !_hasStudioAt(studio.directory, newerThan: studio.version)) { + studios.removeWhere( + (AndroidStudio other) => other.directory == studio.directory); + studios.add(studio); + } + } + } + + String configuredStudioDir = config.getValue('android-studio-dir'); + if (configuredStudioDir != null && !_hasStudioAt(configuredStudioDir)) { + studios.add(new AndroidStudio(configuredStudioDir, + configured: configuredStudioDir)); + } + + if (platform.isLinux) { + void _checkWellKnownPath(String path) { + if (fs.isDirectorySync(path) && !_hasStudioAt(path)) { + studios.add(new AndroidStudio(path)); + } + } + + // Add /opt/android-studio and $HOME/android-studio, if they exist. + _checkWellKnownPath('/opt/android-studio'); + _checkWellKnownPath('$homeDirPath/android-studio'); + } + return studios; + } + + void _init() { + _isValid = false; + _validationMessages.clear(); + + if (configured != null) { + _validationMessages.add('android-studio-dir = $configured'); + } + + if (!fs.isDirectorySync(directory)) { + _validationMessages.add('Android Studio not found at $directory'); + return; + } + + Version latestGradleVersion; + + List gradlePaths; + try { + gradlePaths = fs.directory(fs.path.join(directory, 'gradle')).listSync(); + for (FileSystemEntity entry in gradlePaths.where((FileSystemEntity e) => + e.basename.startsWith('gradle-') && e is Directory)) { + Version version = + new Version.parse(entry.basename.substring('gradle-'.length)); + if (latestGradleVersion == null || version > latestGradleVersion) { + latestGradleVersion = version; + if (version >= minGradleVersion) { + _gradlePath = entry.path; + } + } + } + } catch (e) { + printTrace('Unable to determine Gradle version: $e'); + } + + if (latestGradleVersion == null) { + _validationMessages.add('Gradle not found.'); + } else if (_gradlePath == null) { + _validationMessages.add('Gradle version $minGradleVersion required. ' + 'Found version $latestGradleVersion.'); + } else if (processManager.canRun(gradleExecutable)) { + _isValid = true; + _validationMessages.add('Gradle version $latestGradleVersion'); + } else { + _validationMessages.add( + 'Gradle version $latestGradleVersion at $_gradlePath is not executable.'); + } + } + + @override + String toString() => + version == '0.0' ? 'Android Studio (unknown)' : 'Android Studio $version'; +} diff --git a/packages/flutter_tools/lib/src/android/android_studio_validator.dart b/packages/flutter_tools/lib/src/android/android_studio_validator.dart new file mode 100644 index 0000000000..86573c7543 --- /dev/null +++ b/packages/flutter_tools/lib/src/android/android_studio_validator.dart @@ -0,0 +1,136 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:pub_semver/pub_semver.dart'; + +import '../base/file_system.dart'; +import '../base/io.dart'; +import '../doctor.dart'; +import '../globals.dart'; +import '../base/platform.dart'; +import '../base/process_manager.dart'; +import 'android_studio.dart'; + +class AndroidStudioValidator extends DoctorValidator { + final AndroidStudio _studio; + + AndroidStudioValidator(this._studio) : super('Android Studio'); + + static List get allValidators { + List validators = []; + List studios = AndroidStudio.allInstalled(); + if (studios.isEmpty) { + validators.add(new NoAndroidStudioValidator()); + } else { + validators.addAll(studios + .map((AndroidStudio studio) => new AndroidStudioValidator(studio))); + } + String cfgGradleDir = config.getValue('gradle-dir'); + if (cfgGradleDir != null) { + validators.add(new ConfiguredGradleValidator(cfgGradleDir)); + } + return validators; + } + + @override + Future validate() async { + List messages = []; + ValidationType type = ValidationType.missing; + String studioVersionText = _studio.version == '0.0' + ? 'unknown version' + : 'version ${_studio.version}'; + messages + .add(new ValidationMessage('Android Studio at ${_studio.directory}')); + if (_studio.isValid) { + type = ValidationType.installed; + messages.addAll(_studio.validationMessages + .map((String m) => new ValidationMessage(m))); + } else { + type = ValidationType.partial; + messages.addAll(_studio.validationMessages + .map((String m) => new ValidationMessage.error(m))); + messages.add(new ValidationMessage( + 'Try updating or re-installing Android Studio.')); + if (_studio.configured != null) { + messages.add(new ValidationMessage( + 'Consider removing the android-studio-dir setting.')); + } + } + + return new ValidationResult(type, messages, statusInfo: studioVersionText); + } +} + +class NoAndroidStudioValidator extends DoctorValidator { + NoAndroidStudioValidator() : super('Android Studio'); + + @override + Future validate() async { + List messages = []; + + String cfgAndroidStudio = config.getValue('android-studio-dir'); + if (cfgAndroidStudio != null) { + messages.add( + new ValidationMessage.error('android-studio-dir = $cfgAndroidStudio\n' + 'but Android Studio not found at this location.')); + } + messages.add(new ValidationMessage( + 'Android Studio not found. Download from https://developer.android.com/studio/index.html\n' + '(or visit https://flutter.io/setup/#android-setup for detailed instructions).')); + + return new ValidationResult(ValidationType.missing, messages, + statusInfo: 'not installed'); + } +} + +class ConfiguredGradleValidator extends DoctorValidator { + final String cfgGradleDir; + + ConfiguredGradleValidator(this.cfgGradleDir) : super('Gradle'); + + @override + Future validate() async { + ValidationType type = ValidationType.missing; + List messages = []; + + messages.add(new ValidationMessage('gradle-dir = $cfgGradleDir')); + + String gradleExecutable = cfgGradleDir; + if (!fs.isFileSync(cfgGradleDir)) { + gradleExecutable = fs.path.join( + cfgGradleDir, 'bin', platform.isWindows ? 'gradle.bat' : 'gradle'); + } + String version; + if (processManager.canRun(gradleExecutable)) { + type = ValidationType.partial; + ProcessResult result = + processManager.runSync([gradleExecutable, '--version']); + if (result.exitCode == 0) { + version = result.stdout + .toString() + .split('\n') + .firstWhere((String s) => s.startsWith('Gradle ')) + .substring('Gradle '.length); + if (new Version.parse(version) >= minGradleVersion) { + type = ValidationType.installed; + } else { + messages.add(new ValidationMessage.error( + 'Gradle version $minGradleVersion required. Found version $version.')); + } + } else { + messages + .add(new ValidationMessage('Unable to determine Gradle version.')); + } + } else { + messages + .add(new ValidationMessage('Gradle not found at $gradleExecutable')); + } + + messages.add(new ValidationMessage( + 'Consider removing the gradle-dir setting to use Gradle from Android Studio.')); + return new ValidationResult(type, messages, statusInfo: version); + } +} diff --git a/packages/flutter_tools/lib/src/android/android_workflow.dart b/packages/flutter_tools/lib/src/android/android_workflow.dart index 2ad9b4d2ec..0999d612a1 100644 --- a/packages/flutter_tools/lib/src/android/android_workflow.dart +++ b/packages/flutter_tools/lib/src/android/android_workflow.dart @@ -35,11 +35,11 @@ class AndroidWorkflow extends DoctorValidator implements Workflow { String androidHomeDir = platform.environment[kAndroidHome]; messages.add(new ValidationMessage.error( '$kAndroidHome = $androidHomeDir\n' - 'but Android Studio / Android SDK not found at this location.' + 'but Android SDK not found at this location.' )); } else { messages.add(new ValidationMessage.error( - 'Android Studio / Android SDK not found. Download from https://developer.android.com/sdk/\n' + 'Android SDK not found. Download from https://developer.android.com/sdk/\n' '(or visit https://flutter.io/setup/#android-setup for detailed instructions).' )); } @@ -63,7 +63,7 @@ class AndroidWorkflow extends DoctorValidator implements Workflow { } List validationResult = androidSdk.validateSdkWellFormed(); - // Empty result means SDK is well formated. + // Empty result means SDK is well formed. if (validationResult.isEmpty) { const String _kJdkDownload = 'https://www.oracle.com/technetwork/java/javase/downloads/'; diff --git a/packages/flutter_tools/lib/src/android/gradle.dart b/packages/flutter_tools/lib/src/android/gradle.dart index 4befb55f54..5b2d0ee78a 100644 --- a/packages/flutter_tools/lib/src/android/gradle.dart +++ b/packages/flutter_tools/lib/src/android/gradle.dart @@ -15,6 +15,7 @@ import '../build_info.dart'; import '../cache.dart'; import '../globals.dart'; import 'android_sdk.dart'; +import 'android_studio.dart'; const String gradleManifestPath = 'android/app/src/main/AndroidManifest.xml'; const String gradleAppOutV1 = 'android/app/build/outputs/apk/app-debug.apk'; @@ -67,7 +68,7 @@ String get gradleAppOut { } String locateSystemGradle({ bool ensureExecutable: true }) { - String gradle = _locateSystemGradle(); + String gradle = gradleExecutable; if (ensureExecutable && gradle != null) { File file = fs.file(gradle); if (file.existsSync()) @@ -76,51 +77,6 @@ String locateSystemGradle({ bool ensureExecutable: true }) { return gradle; } -String _locateSystemGradle() { - // See if the user has explicitly configured gradle-dir. - String gradleDir = config.getValue('gradle-dir'); - if (gradleDir != null) { - if (fs.isFileSync(gradleDir)) - return gradleDir; - return fs.path.join(gradleDir, 'bin', 'gradle'); - } - - // Look relative to Android Studio. - String studioPath = config.getValue('android-studio-dir'); - - if (studioPath == null && os.isMacOS) { - final String kDefaultMacPath = '/Applications/Android Studio.app'; - if (fs.isDirectorySync(kDefaultMacPath)) - studioPath = kDefaultMacPath; - } - - if (studioPath != null) { - // '/Applications/Android Studio.app/Contents/gradle/gradle-2.10/bin/gradle' - if (os.isMacOS && !studioPath.endsWith('Contents')) - studioPath = fs.path.join(studioPath, 'Contents'); - - Directory dir = fs.directory(fs.path.join(studioPath, 'gradle')); - if (dir.existsSync()) { - // We find the first valid gradle directory. - for (FileSystemEntity entity in dir.listSync()) { - if (entity is Directory && fs.path.basename(entity.path).startsWith('gradle-')) { - String executable = fs.path.join(entity.path, 'bin', 'gradle'); - if (fs.isFileSync(executable)) - return executable; - } - } - } - } - - // Use 'which'. - File file = os.which('gradle'); - if (file != null) - return file.path; - - // We couldn't locate gradle. - return null; -} - String locateProjectGradlew({ bool ensureExecutable: true }) { final String path = 'android/gradlew'; @@ -133,43 +89,16 @@ String locateProjectGradlew({ bool ensureExecutable: true }) { } } -Future ensureGradlew() async { - String gradlew = locateProjectGradlew(); - - if (gradlew == null) { - String gradle = locateSystemGradle(); +Future ensureGradle() async { + String gradle = locateProjectGradlew(); + if (gradle == null) { + gradle = locateSystemGradle(); if (gradle == null) { - throwToolExit( - 'Unable to locate gradle. Please configure the path to gradle using \'flutter config --gradle-dir\'.' - ); - } else { - printTrace('Using gradle from $gradle.'); + throwToolExit('Unable to locate gradle. Please install Android Studio.'); } - - // Run 'gradle wrapper'. - List command = logger.isVerbose - ? [gradle, 'wrapper'] - : [gradle, '-q', 'wrapper']; - try { - Status status = logger.startProgress('Running \'gradle wrapper\'...', expectSlowOperation: true); - int exitcode = await runCommandAndStreamOutput( - command, - workingDirectory: 'android', - allowReentrantFlutter: true - ); - status.stop(); - if (exitcode != 0) - throwToolExit('Gradle failed: $exitcode', exitCode: exitcode); - } catch (error) { - throwToolExit('$error'); - } - - gradlew = locateProjectGradlew(); - if (gradlew == null) - throwToolExit('Unable to build android/gradlew.'); } - - return gradlew; + printTrace('Using gradle from $gradle.'); + return gradle; } Future buildGradleProject(BuildMode buildMode) async { @@ -190,48 +119,48 @@ Future buildGradleProject(BuildMode buildMode) async { settings.values['flutter.buildMode'] = buildModeName; settings.writeContents(localProperties); - String gradlew = await ensureGradlew(); + String gradle = await ensureGradle(); switch (flutterPluginVersion) { case FlutterPluginVersion.none: // Fall through. Pretend it's v1, and just go for it. case FlutterPluginVersion.v1: - return buildGradleProjectV1(gradlew); + return buildGradleProjectV1(gradle); case FlutterPluginVersion.managed: // Fall through. Managed plugin builds the same way as plugin v2. case FlutterPluginVersion.v2: - return buildGradleProjectV2(gradlew, buildModeName); + return buildGradleProjectV2(gradle, buildModeName); } } String _escapePath(String path) => platform.isWindows ? path.replaceAll('\\', '\\\\') : path; -Future buildGradleProjectV1(String gradlew) async { - // Run 'gradlew build'. - Status status = logger.startProgress('Running \'gradlew build\'...', expectSlowOperation: true); +Future buildGradleProjectV1(String gradle) async { + // Run 'gradle build'. + Status status = logger.startProgress('Running \'gradle build\'...', expectSlowOperation: true); int exitcode = await runCommandAndStreamOutput( - [fs.file(gradlew).absolute.path, 'build'], + [fs.file(gradle).absolute.path, 'build'], workingDirectory: 'android', allowReentrantFlutter: true ); status.stop(); if (exitcode != 0) - throwToolExit('Gradlew failed: $exitcode', exitCode: exitcode); + throwToolExit('Gradle build failed: $exitcode', exitCode: exitcode); File apkFile = fs.file(gradleAppOutV1); printStatus('Built $gradleAppOutV1 (${getSizeAsMB(apkFile.lengthSync())}).'); } -Future buildGradleProjectV2(String gradlew, String buildModeName) async { +Future buildGradleProjectV2(String gradle, String buildModeName) async { String assembleTask = "assemble${toTitleCase(buildModeName)}"; - // Run 'gradlew assemble'. - Status status = logger.startProgress('Running \'gradlew $assembleTask\'...', expectSlowOperation: true); - String gradlewPath = fs.file(gradlew).absolute.path; + // Run 'gradle assemble'. + Status status = logger.startProgress('Running \'gradle $assembleTask\'...', expectSlowOperation: true); + String gradlePath = fs.file(gradle).absolute.path; List command = logger.isVerbose - ? [gradlewPath, assembleTask] - : [gradlewPath, '-q', assembleTask]; + ? [gradlePath, assembleTask] + : [gradlePath, '-q', assembleTask]; int exitcode = await runCommandAndStreamOutput( command, workingDirectory: 'android', @@ -240,7 +169,7 @@ Future buildGradleProjectV2(String gradlew, String buildModeName) async { status.stop(); if (exitcode != 0) - throwToolExit('Gradlew failed: $exitcode', exitCode: exitcode); + throwToolExit('Gradle build failed: $exitcode', exitCode: exitcode); String apkFilename = 'app-$buildModeName.apk'; File apkFile = fs.file('$gradleAppOutDir/$apkFilename'); diff --git a/packages/flutter_tools/lib/src/doctor.dart b/packages/flutter_tools/lib/src/doctor.dart index 204c0bd482..f2bb62d3ed 100644 --- a/packages/flutter_tools/lib/src/doctor.dart +++ b/packages/flutter_tools/lib/src/doctor.dart @@ -8,6 +8,7 @@ import 'dart:convert' show UTF8; import 'package:archive/archive.dart'; import 'android/android_workflow.dart'; +import 'android/android_studio_validator.dart'; import 'base/common.dart'; import 'base/context.dart'; import 'base/file_system.dart'; @@ -15,6 +16,7 @@ import 'base/platform.dart'; import 'device.dart'; import 'globals.dart'; import 'ios/ios_workflow.dart'; +import 'ios/plist_utils.dart'; import 'version.dart'; Doctor get doctor => context[Doctor]; @@ -32,24 +34,8 @@ String osName() { class Doctor { Doctor() { - _validators.add(new _FlutterValidator()); - _androidWorkflow = new AndroidWorkflow(); - if (_androidWorkflow.appliesToHostPlatform) - _validators.add(_androidWorkflow); - _iosWorkflow = new IOSWorkflow(); - if (_iosWorkflow.appliesToHostPlatform) - _validators.add(_iosWorkflow); - - List ideValidators = []; - ideValidators.addAll(IntelliJValidator.installedValidators); - if (ideValidators.isNotEmpty) - _validators.addAll(ideValidators); - else - _validators.add(new NoIdeValidator()); - - _validators.add(new DeviceValidator()); } IOSWorkflow _iosWorkflow; @@ -60,10 +46,34 @@ class Doctor { AndroidWorkflow get androidWorkflow => _androidWorkflow; - List _validators = []; + List _validators; + + List get validators { + if (_validators == null) { + _validators = []; + _validators.add(new _FlutterValidator()); + + if (_androidWorkflow.appliesToHostPlatform) + _validators.add(_androidWorkflow); + + if (_iosWorkflow.appliesToHostPlatform) + _validators.add(_iosWorkflow); + + List ideValidators = []; + ideValidators.addAll(AndroidStudioValidator.allValidators); + ideValidators.addAll(IntelliJValidator.installedValidators); + if (ideValidators.isNotEmpty) + _validators.addAll(ideValidators); + else + _validators.add(new NoIdeValidator()); + + _validators.add(new DeviceValidator()); + } + return _validators; + } List get workflows { - return new List.from(_validators.where((DoctorValidator validator) => validator is Workflow)); + return new List.from(validators.where((DoctorValidator validator) => validator is Workflow)); } /// Print a summary of the state of the tooling, as well as how to get more info. @@ -76,7 +86,7 @@ class Doctor { bool allGood = true; - for (DoctorValidator validator in _validators) { + for (DoctorValidator validator in validators) { ValidationResult result = await validator.validate(); buffer.write('${result.leadingBox} ${validator.title} is '); if (result.type == ValidationType.missing) @@ -108,7 +118,7 @@ class Doctor { bool firstLine = true; bool doctorResult = true; - for (DoctorValidator validator in _validators) { + for (DoctorValidator validator in validators) { if (!firstLine) printStatus(''); firstLine = false; @@ -434,23 +444,8 @@ class IntelliJValidatorOnMac extends IntelliJValidator { @override String get version { if (_version == null) { - String plist; - try { - plist = fs.file(fs.path.join(installPath, 'Contents', 'Info.plist')).readAsStringSync(); - int index = plist.indexOf('CFBundleShortVersionString'); - if (index > 0) { - int start = plist.indexOf('', index); - if (start > 0) { - int end = plist.indexOf('', start); - if (end > 0) { - _version = plist.substring(start + 8, end); - } - } - } - } on FileSystemException catch (_) { - // ignored - } - _version ??= 'unknown'; + String plistFile = fs.path.join(installPath, 'Contents', 'Info.plist'); + _version = getValueFromFile(plistFile, kCFBundleShortVersionStringKey) ?? 'unknown'; } return _version; } diff --git a/packages/flutter_tools/lib/src/ios/plist_utils.dart b/packages/flutter_tools/lib/src/ios/plist_utils.dart index 2392468211..105c18c257 100644 --- a/packages/flutter_tools/lib/src/ios/plist_utils.dart +++ b/packages/flutter_tools/lib/src/ios/plist_utils.dart @@ -6,6 +6,7 @@ import '../base/file_system.dart'; import '../base/process.dart'; const String kCFBundleIdentifierKey = "CFBundleIdentifier"; +const String kCFBundleShortVersionStringKey = "CFBundleShortVersionString"; String getValueFromFile(String plistFilePath, String key) { // TODO(chinmaygarde): For now, we only need to read from plist files on a mac diff --git a/packages/flutter_tools/test/src/context.dart b/packages/flutter_tools/test/src/context.dart index f3230e34ed..deb5e77972 100644 --- a/packages/flutter_tools/test/src/context.dart +++ b/packages/flutter_tools/test/src/context.dart @@ -46,6 +46,7 @@ void testUsingContext(String description, dynamic testMethod(), { testContext.putIfAbsent(FileSystem, () => new LocalFileSystem()); testContext.putIfAbsent(ProcessManager, () => new LocalProcessManager()); testContext.putIfAbsent(Logger, () => new BufferLogger()); + testContext.putIfAbsent(Config, () => new Config()); // Order-independent context entries testContext.putIfAbsent(DeviceManager, () => new MockDeviceManager()); @@ -54,7 +55,6 @@ void testUsingContext(String description, dynamic testMethod(), { testContext.putIfAbsent(HotRunnerConfig, () => new HotRunnerConfig()); testContext.putIfAbsent(Cache, () => new Cache()); testContext.putIfAbsent(Artifacts, () => new CachedArtifacts()); - testContext.putIfAbsent(Config, () => new Config()); testContext.putIfAbsent(OperatingSystemUtils, () { MockOperatingSystemUtils os = new MockOperatingSystemUtils(); when(os.isWindows).thenReturn(false);