ide_config.dart 10.3 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 8 9
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';

import '../base/common.dart';
import '../base/file_system.dart';
import '../cache.dart';
10
import '../globals.dart' as globals;
11 12 13 14
import '../runner/flutter_command.dart';
import '../template.dart';

class IdeConfigCommand extends FlutterCommand {
15
  IdeConfigCommand() {
16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
    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.',
    );
32 33 34 35 36 37 38 39
    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.',
    );
40 41 42 43 44
  }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

247
    return FlutterCommandResult.success();
248 249 250
  }

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

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

273
  if (type != FileSystemEntityType.notFound) {
274
    switch (type) {
275
      case FileSystemEntityType.link:
276 277 278 279 280 281 282
        // Do not overwrite links.
        return "Invalid project root dir: '$dirPath' - refers to a link.";
    }
  }

  return null;
}