| // Copyright 2014 The Flutter 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 '../base/common.dart'; |
| import '../base/file_system.dart'; |
| import '../cache.dart'; |
| import '../globals.dart' as globals; |
| import '../runner/flutter_command.dart'; |
| import '../template.dart'; |
| |
| class IdeConfigCommand extends FlutterCommand { |
| IdeConfigCommand() { |
| argParser.addFlag( |
| 'overwrite', |
| help: 'When performing operations, overwrite existing files.', |
| ); |
| argParser.addFlag( |
| 'update-templates', |
| negatable: false, |
| help: 'Update the templates in the template directory from the current ' |
| 'configuration files. This is the opposite of what $name usually does. ' |
| 'Will search the flutter tree for *.iml files and copy any missing ones ' |
| 'into the template directory. If "--overwrite" is also specified, it will ' |
| 'update any out-of-date files, and remove any deleted files from the ' |
| 'template directory.', |
| ); |
| argParser.addFlag( |
| 'with-root-module', |
| defaultsTo: true, |
| help: 'Also create module that corresponds to the root of Flutter tree. ' |
| 'This makes the entire Flutter tree browsable and searchable in IDE. ' |
| 'Without this flag, only the child modules will be visible in IDE.', |
| ); |
| } |
| |
| @override |
| final String name = 'ide-config'; |
| |
| @override |
| Future<Set<DevelopmentArtifact>> get requiredArtifacts async => const <DevelopmentArtifact>{}; |
| |
| @override |
| final String description = 'Configure the IDE for use in the Flutter tree.\n\n' |
| 'If run on a Flutter tree that is already configured for the IDE, this ' |
| 'command will add any new configurations, recreate any files that are ' |
| 'missing. If --overwrite is specified, will revert existing files to ' |
| 'the template versions, reset the module list, and return configuration ' |
| 'settings to the template versions.\n\n' |
| 'This command is intended for Flutter developers to help them set up the ' |
| "Flutter tree for development in an IDE. It doesn't affect other projects.\n\n" |
| 'Currently, IntelliJ is the default (and only) IDE that may be configured.'; |
| |
| @override |
| final bool hidden = true; |
| |
| @override |
| String get invocation => '${runner?.executableName} $name'; |
| |
| static const String _ideName = 'intellij'; |
| Directory get _templateDirectory { |
| return globals.fs.directory(globals.fs.path.join( |
| Cache.flutterRoot!, |
| 'packages', |
| 'flutter_tools', |
| 'ide_templates', |
| _ideName, |
| )); |
| } |
| |
| Directory get _createTemplatesDirectory { |
| return globals.fs.directory(globals.fs.path.join( |
| Cache.flutterRoot!, |
| 'packages', |
| 'flutter_tools', |
| 'templates', |
| )); |
| } |
| |
| Directory get _flutterRoot => globals.fs.directory(globals.fs.path.absolute(Cache.flutterRoot!)); |
| |
| // Returns true if any entire path element is equal to dir. |
| bool _hasDirectoryInPath(FileSystemEntity entity, String dir) { |
| String path = entity.absolute.path; |
| while (path.isNotEmpty && globals.fs.path.dirname(path) != path) { |
| if (globals.fs.path.basename(path) == dir) { |
| return true; |
| } |
| path = globals.fs.path.dirname(path); |
| } |
| return false; |
| } |
| |
| // Returns true if child is anywhere underneath parent. |
| bool _isChildDirectoryOf(FileSystemEntity parent, FileSystemEntity child) { |
| return child.absolute.path.startsWith(parent.absolute.path); |
| } |
| |
| // Checks the contents of the two files to see if they have changes. |
| bool _fileIsIdentical(File src, File dest) { |
| if (src.lengthSync() != dest.lengthSync()) { |
| return false; |
| } |
| |
| // Test byte by byte. We're assuming that these are small files. |
| final List<int> srcBytes = src.readAsBytesSync(); |
| final List<int> destBytes = dest.readAsBytesSync(); |
| for (int i = 0; i < srcBytes.length; ++i) { |
| if (srcBytes[i] != destBytes[i]) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| // Discovers and syncs with existing configuration files in the Flutter tree. |
| void _handleTemplateUpdate() { |
| if (!_flutterRoot.existsSync()) { |
| return; |
| } |
| |
| final Set<String> manifest = <String>{}; |
| final Iterable<File> flutterFiles = _flutterRoot.listSync(recursive: true).whereType<File>(); |
| for (final File srcFile in flutterFiles) { |
| final String relativePath = globals.fs.path.relative(srcFile.path, from: _flutterRoot.absolute.path); |
| |
| // Skip template files in both the ide_templates and templates |
| // directories to avoid copying onto themselves. |
| if (_isChildDirectoryOf(_templateDirectory, srcFile) || |
| _isChildDirectoryOf(_createTemplatesDirectory, srcFile)) { |
| continue; |
| } |
| |
| // Skip files we aren't interested in. |
| final RegExp trackedIdeaFileRegExp = RegExp( |
| r'(\.name|modules.xml|vcs.xml)$', |
| ); |
| final bool isATrackedIdeaFile = _hasDirectoryInPath(srcFile, '.idea') && |
| (trackedIdeaFileRegExp.hasMatch(relativePath) || |
| _hasDirectoryInPath(srcFile, 'runConfigurations')); |
| final bool isAnImlOutsideIdea = !isATrackedIdeaFile && srcFile.path.endsWith('.iml'); |
| if (!isATrackedIdeaFile && !isAnImlOutsideIdea) { |
| continue; |
| } |
| |
| final File finalDestinationFile = globals.fs.file(globals.fs.path.absolute( |
| _templateDirectory.absolute.path, '$relativePath${Template.copyTemplateExtension}')); |
| final String relativeDestination = |
| globals.fs.path.relative(finalDestinationFile.path, from: _flutterRoot.absolute.path); |
| if (finalDestinationFile.existsSync()) { |
| if (_fileIsIdentical(srcFile, finalDestinationFile)) { |
| globals.printTrace(' $relativeDestination (identical)'); |
| manifest.add('$relativePath${Template.copyTemplateExtension}'); |
| continue; |
| } |
| if (boolArgDeprecated('overwrite')) { |
| finalDestinationFile.deleteSync(); |
| globals.printStatus(' $relativeDestination (overwritten)'); |
| } else { |
| globals.printTrace(' $relativeDestination (existing - skipped)'); |
| manifest.add('$relativePath${Template.copyTemplateExtension}'); |
| continue; |
| } |
| } else { |
| globals.printStatus(' $relativeDestination (added)'); |
| } |
| final Directory finalDestinationDir = globals.fs.directory(finalDestinationFile.dirname); |
| if (!finalDestinationDir.existsSync()) { |
| globals.printTrace(" ${finalDestinationDir.path} doesn't exist, creating."); |
| finalDestinationDir.createSync(recursive: true); |
| } |
| srcFile.copySync(finalDestinationFile.path); |
| manifest.add('$relativePath${Template.copyTemplateExtension}'); |
| } |
| |
| // If we're not overwriting, then we're not going to remove missing items either. |
| if (!boolArgDeprecated('overwrite')) { |
| return; |
| } |
| |
| // Look for any files under the template dir that don't exist in the manifest and remove |
| // them. |
| final Iterable<File> templateFiles = _templateDirectory.listSync(recursive: true).whereType<File>(); |
| for (final File templateFile in templateFiles) { |
| final String relativePath = globals.fs.path.relative( |
| templateFile.absolute.path, |
| from: _templateDirectory.absolute.path, |
| ); |
| if (!manifest.contains(relativePath)) { |
| templateFile.deleteSync(); |
| final String relativeDestination = |
| globals.fs.path.relative(templateFile.path, from: _flutterRoot.absolute.path); |
| globals.printStatus(' $relativeDestination (removed)'); |
| } |
| // If the directory is now empty, then remove it, and do the same for its parent, |
| // until we escape to the template directory. |
| Directory parentDir = globals.fs.directory(templateFile.dirname); |
| while (parentDir.listSync().isEmpty) { |
| parentDir.deleteSync(); |
| globals.printTrace(' ${globals.fs.path.relative(parentDir.absolute.path)} (empty directory - removed)'); |
| parentDir = globals.fs.directory(parentDir.dirname); |
| if (globals.fs.path.isWithin(_templateDirectory.absolute.path, parentDir.absolute.path)) { |
| break; |
| } |
| } |
| } |
| } |
| |
| @override |
| Future<FlutterCommandResult> runCommand() async { |
| final List<String> rest = argResults?.rest ?? <String>[]; |
| if (rest.isNotEmpty) { |
| throwToolExit('Currently, the only supported IDE is IntelliJ\n$usage', exitCode: 2); |
| } |
| |
| if (boolArgDeprecated('update-templates')) { |
| _handleTemplateUpdate(); |
| return FlutterCommandResult.success(); |
| } |
| |
| final String flutterRoot = globals.fs.path.absolute(Cache.flutterRoot!); |
| final String dirPath = globals.fs.path.normalize( |
| globals.fs.directory(globals.fs.path.absolute(Cache.flutterRoot!)).absolute.path, |
| ); |
| |
| final String? error = _validateFlutterDir(dirPath, flutterRoot: flutterRoot); |
| if (error != null) { |
| throwToolExit(error); |
| } |
| |
| globals.printStatus('Updating IDE configuration for Flutter tree at $dirPath...'); |
| int generatedCount = 0; |
| generatedCount += _renderTemplate(_ideName, dirPath, <String, Object>{ |
| 'withRootModule': boolArgDeprecated('with-root-module'), |
| 'android': true, |
| }); |
| |
| globals.printStatus('Wrote $generatedCount files.'); |
| globals.printStatus(''); |
| globals.printStatus('Your IntelliJ configuration is now up to date. It is prudent to ' |
| 'restart IntelliJ, if running.'); |
| |
| return FlutterCommandResult.success(); |
| } |
| |
| int _renderTemplate(String templateName, String dirPath, Map<String, Object> context) { |
| final Template template = Template( |
| _templateDirectory, |
| null, |
| fileSystem: globals.fs, |
| logger: globals.logger, |
| templateRenderer: globals.templateRenderer, |
| ); |
| return template.render( |
| globals.fs.directory(dirPath), |
| context, |
| overwriteExisting: boolArgDeprecated('overwrite'), |
| ); |
| } |
| } |
| |
| /// Return null if the flutter root directory is a valid destination. Return a |
| /// validation message if we should disallow the directory. |
| String? _validateFlutterDir(String dirPath, { String? flutterRoot }) { |
| final FileSystemEntityType type = globals.fs.typeSync(dirPath); |
| |
| switch (type) { // ignore: exhaustive_cases, https://github.com/dart-lang/linter/issues/3017 |
| case FileSystemEntityType.link: |
| // Do not overwrite links. |
| return "Invalid project root dir: '$dirPath' - refers to a link."; |
| case FileSystemEntityType.file: |
| case FileSystemEntityType.directory: |
| case FileSystemEntityType.notFound: |
| return null; |
| } |
| // In the case of any other [FileSystemEntityType]s, like the deprecated ones, return null. |
| return null; |
| } |