ide_config.dart 10.5 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
    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 '
22 23 24 25 26
            '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.',
27
    );
28 29 30 31
    argParser.addFlag(
      'with-root-module',
      defaultsTo: true,
      help: 'Also create module that corresponds to the root of Flutter tree. '
32 33
            'This makes the entire Flutter tree browsable and searchable in IDE. '
            'Without this flag, only the child modules will be visible in IDE.',
34
    );
35 36 37 38 39
  }

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

40 41 42
  @override
  Future<Set<DevelopmentArtifact>> get requiredArtifacts async => const <DevelopmentArtifact>{};

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

  @override
58
  String get invocation => '${runner?.executableName} $name';
59 60 61

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

  Directory get _createTemplatesDirectory {
72
    return globals.fs.directory(globals.fs.path.join(
73
      Cache.flutterRoot!,
74 75 76 77 78 79
      'packages',
      'flutter_tools',
      'templates',
    ));
  }

80
  Directory get _flutterRoot => globals.fs.directory(globals.fs.path.absolute(Cache.flutterRoot!));
81 82 83 84

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

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

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

      // 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.
135
      final RegExp trackedIdeaFileRegExp = RegExp(
136 137 138
        r'(\.name|modules.xml|vcs.xml)$',
      );
      final bool isATrackedIdeaFile = _hasDirectoryInPath(srcFile, '.idea') &&
139
          (trackedIdeaFileRegExp.hasMatch(relativePath) ||
140 141 142 143 144 145
              _hasDirectoryInPath(srcFile, 'runConfigurations'));
      final bool isAnImlOutsideIdea = !isATrackedIdeaFile && srcFile.path.endsWith('.iml');
      if (!isATrackedIdeaFile && !isAnImlOutsideIdea) {
        continue;
      }

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

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

  @override
210
  Future<FlutterCommandResult> runCommand() async {
211 212
    final List<String> rest = argResults?.rest ?? <String>[];
    if (rest.isNotEmpty) {
213 214 215
      throwToolExit('Currently, the only supported IDE is IntelliJ\n$usage', exitCode: 2);
    }

216
    if (boolArgDeprecated('update-templates')) {
217
      _handleTemplateUpdate();
218
      return FlutterCommandResult.success();
219 220
    }

221
    final String flutterRoot = globals.fs.path.absolute(Cache.flutterRoot!);
222
    final String dirPath = globals.fs.path.normalize(
223
      globals.fs.directory(globals.fs.path.absolute(Cache.flutterRoot!)).absolute.path,
224 225
    );

226
    final String? error = _validateFlutterDir(dirPath, flutterRoot: flutterRoot);
227 228 229 230
    if (error != null) {
      throwToolExit(error);
    }

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

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

243
    return FlutterCommandResult.success();
244 245
  }

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

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

267
  switch (type) { // ignore: exhaustive_cases, https://github.com/dart-lang/linter/issues/3017
268 269 270
    case FileSystemEntityType.link:
      // Do not overwrite links.
      return "Invalid project root dir: '$dirPath' - refers to a link.";
271 272 273
    case FileSystemEntityType.file:
    case FileSystemEntityType.directory:
    case FileSystemEntityType.notFound:
274
      return null;
275
  }
276 277
  // In the case of any other [FileSystemEntityType]s, like the deprecated ones, return null.
  return null;
278
}