ide_config.dart 10.2 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5 6 7
// 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';
8
import '../globals.dart' as globals;
9 10 11 12
import '../runner/flutter_command.dart';
import '../template.dart';

class IdeConfigCommand extends FlutterCommand {
13
  IdeConfigCommand() {
14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
    argParser.addFlag(
      'overwrite',
      negatable: true,
      defaultsTo: false,
      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.',
    );
30 31 32 33 34 35 36 37
    argParser.addFlag(
      'with-root-module',
      negatable: true,
      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.',
    );
38 39 40 41 42
  }

  @override
  final String name = 'ide-config';

43 44 45
  @override
  Future<Set<DevelopmentArtifact>> get requiredArtifacts async => const <DevelopmentArtifact>{};

46 47 48 49 50 51 52
  @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'
53
      'This command is intended for Flutter developers to help them set up the '
54 55 56 57
      "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
58
  final bool hidden = true;
59 60 61 62 63 64

  @override
  String get invocation => '${runner.executableName} $name';

  static const String _ideName = 'intellij';
  Directory get _templateDirectory {
65
    return globals.fs.directory(globals.fs.path.join(
66 67 68 69 70 71 72 73 74
      Cache.flutterRoot,
      'packages',
      'flutter_tools',
      'ide_templates',
      _ideName,
    ));
  }

  Directory get _createTemplatesDirectory {
75
    return globals.fs.directory(globals.fs.path.join(
76 77 78 79 80 81 82
      Cache.flutterRoot,
      'packages',
      'flutter_tools',
      'templates',
    ));
  }

83
  Directory get _flutterRoot => globals.fs.directory(globals.fs.path.absolute(Cache.flutterRoot));
84 85 86 87

  // Returns true if any entire path element is equal to dir.
  bool _hasDirectoryInPath(FileSystemEntity entity, String dir) {
    String path = entity.absolute.path;
88 89
    while (path.isNotEmpty && globals.fs.path.dirname(path) != path) {
      if (globals.fs.path.basename(path) == dir) {
90 91
        return true;
      }
92
      path = globals.fs.path.dirname(path);
93 94 95 96 97 98 99 100 101 102 103 104 105 106 107
    }
    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;
    }

108
    // Test byte by byte. We're assuming that these are small files.
109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124
    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;
    }

125
    final Set<String> manifest = <String>{};
126
    final Iterable<File> flutterFiles = _flutterRoot.listSync(recursive: true).whereType<File>();
127
    for (final File srcFile in flutterFiles) {
128
      final String relativePath = globals.fs.path.relative(srcFile.path, from: _flutterRoot.absolute.path);
129 130 131 132 133 134 135 136 137

      // 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.
138
      final RegExp _trackedIdeaFileRegExp = RegExp(
139 140 141 142 143 144 145 146 147 148
        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;
      }

149
      final File finalDestinationFile = globals.fs.file(globals.fs.path.absolute(
150 151
          _templateDirectory.absolute.path, '$relativePath${Template.copyTemplateExtension}'));
      final String relativeDestination =
152
          globals.fs.path.relative(finalDestinationFile.path, from: _flutterRoot.absolute.path);
153 154
      if (finalDestinationFile.existsSync()) {
        if (_fileIsIdentical(srcFile, finalDestinationFile)) {
155
          globals.printTrace('  $relativeDestination (identical)');
156 157 158
          manifest.add('$relativePath${Template.copyTemplateExtension}');
          continue;
        }
159
        if (boolArg('overwrite')) {
160
          finalDestinationFile.deleteSync();
161
          globals.printStatus('  $relativeDestination (overwritten)');
162
        } else {
163
          globals.printTrace('  $relativeDestination (existing - skipped)');
164 165 166 167
          manifest.add('$relativePath${Template.copyTemplateExtension}');
          continue;
        }
      } else {
168
        globals.printStatus('  $relativeDestination (added)');
169
      }
170
      final Directory finalDestinationDir = globals.fs.directory(finalDestinationFile.dirname);
171
      if (!finalDestinationDir.existsSync()) {
172
        globals.printTrace("  ${finalDestinationDir.path} doesn't exist, creating.");
173 174 175 176 177 178 179
        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.
180
    if (!boolArg('overwrite')) {
181 182 183 184 185
      return;
    }

    // Look for any files under the template dir that don't exist in the manifest and remove
    // them.
186
    final Iterable<File> templateFiles = _templateDirectory.listSync(recursive: true).whereType<File>();
187
    for (final File templateFile in templateFiles) {
188
      final String relativePath = globals.fs.path.relative(
189 190 191 192 193 194
        templateFile.absolute.path,
        from: _templateDirectory.absolute.path,
      );
      if (!manifest.contains(relativePath)) {
        templateFile.deleteSync();
        final String relativeDestination =
195 196
            globals.fs.path.relative(templateFile.path, from: _flutterRoot.absolute.path);
        globals.printStatus('  $relativeDestination (removed)');
197 198 199
      }
      // If the directory is now empty, then remove it, and do the same for its parent,
      // until we escape to the template directory.
200
      Directory parentDir = globals.fs.directory(templateFile.dirname);
201 202
      while (parentDir.listSync().isEmpty) {
        parentDir.deleteSync();
203 204 205
        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)) {
206 207 208 209 210 211 212
          break;
        }
      }
    }
  }

  @override
213
  Future<FlutterCommandResult> runCommand() async {
214 215 216 217
    if (argResults.rest.isNotEmpty) {
      throwToolExit('Currently, the only supported IDE is IntelliJ\n$usage', exitCode: 2);
    }

218
    if (boolArg('update-templates')) {
219
      _handleTemplateUpdate();
220
      return FlutterCommandResult.success();
221 222
    }

223 224 225
    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,
226 227 228 229 230 231 232
    );

    final String error = _validateFlutterDir(dirPath, flutterRoot: flutterRoot);
    if (error != null) {
      throwToolExit(error);
    }

233
    globals.printStatus('Updating IDE configuration for Flutter tree at $dirPath...');
234
    int generatedCount = 0;
235
    generatedCount += _renderTemplate(_ideName, dirPath, <String, dynamic>{
236
      'withRootModule': boolArg('with-root-module'),
237
      'android': true,
238
    });
239

240 241 242
    globals.printStatus('Wrote $generatedCount files.');
    globals.printStatus('');
    globals.printStatus('Your IntelliJ configuration is now up to date. It is prudent to '
243
        'restart IntelliJ, if running.');
244

245
    return FlutterCommandResult.success();
246 247 248
  }

  int _renderTemplate(String templateName, String dirPath, Map<String, dynamic> context) {
249 250 251 252 253 254
    final Template template = Template(
      _templateDirectory,
      _templateDirectory,
      null,
      fileSystem: globals.fs,
      templateManifest: null,
255 256
      logger: globals.logger,
      templateRenderer: globals.templateRenderer,
257
    );
258
    return template.render(
259
      globals.fs.directory(dirPath),
260
      context,
261
      overwriteExisting: boolArg('overwrite'),
262 263 264 265 266 267
    );
  }
}

/// Return null if the flutter root directory is a valid destination. Return a
/// validation message if we should disallow the directory.
268
String _validateFlutterDir(String dirPath, { String flutterRoot }) {
269
  final FileSystemEntityType type = globals.fs.typeSync(dirPath);
270

271 272 273 274 275 276
  switch (type) {
    case FileSystemEntityType.link:
      // Do not overwrite links.
      return "Invalid project root dir: '$dirPath' - refers to a link.";
    default:
      return null;
277 278
  }
}