template.dart 10.8 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'package:meta/meta.dart';
6 7
import 'package:package_config/package_config.dart';
import 'package:package_config/package_config_types.dart';
8

9
import 'base/common.dart';
10
import 'base/file_system.dart';
11
import 'cache.dart';
12 13 14
import 'dart/package_map.dart';
import 'dart/pub.dart';
import 'globals.dart' as globals hide fs;
15 16

/// Expands templates in a directory to a destination. All files that must
17 18 19
/// undergo template expansion should end with the '.tmpl' extension. All files
/// that should be replaced with the corresponding image from
/// flutter_template_images should end with the '.img.tmpl' extension. All other
20 21
/// files are ignored. In case the contents of entire directories must be copied
/// as is, the directory itself can end with '.tmpl' extension. Files within
22 23 24
/// such a directory may also contain the '.tmpl' or '.img.tmpl' extensions and
/// will be considered for expansion. In case certain files need to be copied
/// but without template expansion (data files, etc.), the '.copy.tmpl'
25 26
/// extension may be used.
///
27 28 29
/// Folders with platform/language-specific content must be named
/// '<platform>-<language>.tmpl'.
///
30 31
/// Files in the destination will contain none of the '.tmpl', '.copy.tmpl',
/// 'img.tmpl', or '-<language>.tmpl' extensions.
32
class Template {
33 34 35
  Template(Directory templateSource, Directory baseDir, this.imageSourceDir, {
    @required FileSystem fileSystem,
  }) : _fileSystem = fileSystem {
36
    _templateFilePaths = <String, String>{};
37 38 39 40 41

    if (!templateSource.existsSync()) {
      return;
    }

42
    final List<FileSystemEntity> templateFiles = templateSource.listSync(recursive: true);
43

44
    for (final FileSystemEntity entity in templateFiles) {
45 46 47 48 49
      if (entity is! File) {
        // We are only interesting in template *file* URIs.
        continue;
      }

50
      final String relativePath = fileSystem.path.relative(entity.path,
51 52
          from: baseDir.absolute.path);

53
      if (relativePath.contains(templateExtension)) {
54 55 56
        // If '.tmpl' appears anywhere within the path of this entity, it is
        // is a candidate for rendering. This catches cases where the folder
        // itself is a template.
57
        _templateFilePaths[relativePath] = fileSystem.path.absolute(entity.path);
58 59 60 61
      }
    }
  }

62
  static Future<Template> fromName(String name, { @required FileSystem fileSystem }) async {
Ian Hickson's avatar
Ian Hickson committed
63
    // All named templates are placed in the 'templates' directory
64 65 66
    final Directory templateDir = _templateDirectoryInPackage(name, fileSystem);
    final Directory imageDir = await _templateImageDirectory(name, fileSystem);
    return Template(templateDir, templateDir, imageDir, fileSystem: fileSystem);
Ian Hickson's avatar
Ian Hickson committed
67 68
  }

69
  final FileSystem _fileSystem;
70 71
  static const String templateExtension = '.tmpl';
  static const String copyTemplateExtension = '.copy.tmpl';
72
  static const String imageTemplateExtension = '.img.tmpl';
73
  final Pattern _kTemplateLanguageVariant = RegExp(r'(\w+)-(\w+)\.tmpl.*');
74
  final Directory imageSourceDir;
75

76 77
  Map<String /* relative */, String /* absolute source */> _templateFilePaths;

78 79 80
  /// Render the template into [directory].
  ///
  /// May throw a [ToolExit] if the directory is not writable.
81 82 83
  int render(
    Directory destination,
    Map<String, dynamic> context, {
84
    bool overwriteExisting = true,
85
    bool printStatusWhenWriting = true,
86
  }) {
87 88 89
    try {
      destination.createSync(recursive: true);
    } on FileSystemException catch (err) {
90
      globals.printError(err.toString());
91 92 93
      throwToolExit('Failed to flutter create at ${destination.path}.');
      return 0;
    }
Devon Carew's avatar
Devon Carew committed
94
    int fileCount = 0;
95

96 97 98 99 100 101 102 103 104
    /// Returns the resolved destination path corresponding to the specified
    /// raw destination path, after performing language filtering and template
    /// expansion on the path itself.
    ///
    /// Returns null if the given raw destination path has been filtered.
    String renderPath(String relativeDestinationPath) {
      final Match match = _kTemplateLanguageVariant.matchAsPrefix(relativeDestinationPath);
      if (match != null) {
        final String platform = match.group(1);
105
        final String language = context['${platform}Language'] as String;
106
        if (language != match.group(2)) {
107
          return null;
108
        }
109 110
        relativeDestinationPath = relativeDestinationPath.replaceAll('$platform-$language.tmpl', platform);
      }
111
      // Only build a web project if explicitly asked.
112
      final bool web = context['web'] as bool;
113 114 115
      if (relativeDestinationPath.contains('web') && !web) {
        return null;
      }
116 117 118 119 120
      // Only build a Linux project if explicitly asked.
      final bool linux = context['linux'] as bool;
      if (relativeDestinationPath.startsWith('linux.tmpl') && !linux) {
        return null;
      }
121
      // Only build a macOS project if explicitly asked.
122
      final bool macOS = context['macos'] as bool;
123 124 125
      if (relativeDestinationPath.startsWith('macos.tmpl') && !macOS) {
        return null;
      }
126 127 128 129 130
      // Only build a Windows project if explicitly asked.
      final bool windows = context['windows'] as bool;
      if (relativeDestinationPath.startsWith('windows.tmpl') && !windows) {
        return null;
      }
131 132 133
      final String projectName = context['projectName'] as String;
      final String androidIdentifier = context['androidIdentifier'] as String;
      final String pluginClass = context['pluginClass'] as String;
134
      final String destinationDirPath = destination.absolute.path;
135 136
      final String pathSeparator = _fileSystem.path.separator;
      String finalDestinationPath = _fileSystem.path
137
        .join(destinationDirPath, relativeDestinationPath)
138
        .replaceAll(copyTemplateExtension, '')
139
        .replaceAll(imageTemplateExtension, '')
140
        .replaceAll(templateExtension, '');
141 142 143 144 145

      if (androidIdentifier != null) {
        finalDestinationPath = finalDestinationPath
            .replaceAll('androidIdentifier', androidIdentifier.replaceAll('.', pathSeparator));
      }
146
      if (projectName != null) {
147
        finalDestinationPath = finalDestinationPath.replaceAll('projectName', projectName);
148 149
      }
      if (pluginClass != null) {
150
        finalDestinationPath = finalDestinationPath.replaceAll('pluginClass', pluginClass);
151
      }
152 153 154 155
      return finalDestinationPath;
    }

    _templateFilePaths.forEach((String relativeDestinationPath, String absoluteSourcePath) {
156
      final bool withRootModule = context['withRootModule'] as bool ?? false;
157
      if (!withRootModule && absoluteSourcePath.contains('flutter_root')) {
158
        return;
159
      }
160

161
      final String finalDestinationPath = renderPath(relativeDestinationPath);
162
      if (finalDestinationPath == null) {
163
        return;
164
      }
165 166
      final File finalDestinationFile = _fileSystem.file(finalDestinationPath);
      final String relativePathForLogging = _fileSystem.path.relative(finalDestinationFile.path);
167 168 169 170 171

      // Step 1: Check if the file needs to be overwritten.

      if (finalDestinationFile.existsSync()) {
        if (overwriteExisting) {
172
          finalDestinationFile.deleteSync(recursive: true);
173
          if (printStatusWhenWriting) {
174
            globals.printStatus('  $relativePathForLogging (overwritten)');
175
          }
176 177
        } else {
          // The file exists but we cannot overwrite it, move on.
178
          if (printStatusWhenWriting) {
179
            globals.printTrace('  $relativePathForLogging (existing - skipped)');
180
          }
181 182 183
          return;
        }
      } else {
184
        if (printStatusWhenWriting) {
185
          globals.printStatus('  $relativePathForLogging (created)');
186
        }
187 188
      }

Devon Carew's avatar
Devon Carew committed
189 190
      fileCount++;

191
      finalDestinationFile.createSync(recursive: true);
192
      final File sourceFile = _fileSystem.file(absoluteSourcePath);
193

194
      // Step 2: If the absolute paths ends with a '.copy.tmpl', this file does
195 196
      //         not need mustache rendering but needs to be directly copied.

197 198
      if (sourceFile.path.endsWith(copyTemplateExtension)) {
        sourceFile.copySync(finalDestinationFile.path);
199 200 201 202

        return;
      }

203 204 205 206 207 208 209 210 211 212 213 214
      // Step 3: If the absolute paths ends with a '.img.tmpl', this file needs
      //         to be copied from the template image package.

      if (sourceFile.path.endsWith(imageTemplateExtension)) {
        final File imageSourceFile = _fileSystem.file(_fileSystem.path.join(
            imageSourceDir.path, relativeDestinationPath.replaceAll(imageTemplateExtension, '')));
        imageSourceFile.copySync(finalDestinationFile.path);

        return;
      }

      // Step 4: If the absolute path ends with a '.tmpl', this file needs
215 216
      //         rendering via mustache.

217
      if (sourceFile.path.endsWith(templateExtension)) {
218
        final String templateContents = sourceFile.readAsStringSync();
219
        final String renderedContents = globals.templateRenderer.renderString(templateContents, context);
220 221 222 223 224 225

        finalDestinationFile.writeAsStringSync(renderedContents);

        return;
      }

226
      // Step 5: This file does not end in .tmpl but is in a directory that
227 228
      //         does. Directly copy the file to the destination.

229
      sourceFile.copySync(finalDestinationFile.path);
230
    });
Devon Carew's avatar
Devon Carew committed
231 232

    return fileCount;
233 234 235
  }
}

236 237
Directory _templateDirectoryInPackage(String name, FileSystem fileSystem) {
  final String templatesDir = fileSystem.path.join(Cache.flutterRoot,
238
      'packages', 'flutter_tools', 'templates');
239 240 241 242 243 244 245 246 247 248 249 250 251
  return fileSystem.directory(fileSystem.path.join(templatesDir, name));
}

// Returns the directory containing the 'name' template directory in
// flutter_template_images, to resolve image placeholder against.
Future<Directory> _templateImageDirectory(String name, FileSystem fileSystem) async {
  final String toolPackagePath = fileSystem.path.join(
      Cache.flutterRoot, 'packages', 'flutter_tools');
  final String packageFilePath = fileSystem.path.join(toolPackagePath, kPackagesFileName);
  // Ensure that .packgaes is present.
  if (!fileSystem.file(packageFilePath).existsSync()) {
    await _ensurePackageDependencies(toolPackagePath);
  }
252
  final PackageConfig packageConfig = await loadPackageConfigWithLogging(
253 254 255 256
    fileSystem.file(packageFilePath),
    logger: globals.logger,
  );
  final Uri imagePackageLibDir = packageConfig['flutter_template_images']?.packageUriRoot;
257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273
  // Ensure that the template image package is present.
  if (imagePackageLibDir == null || !fileSystem.directory(imagePackageLibDir).existsSync()) {
    await _ensurePackageDependencies(toolPackagePath);
  }
  return fileSystem.directory(imagePackageLibDir)
      .parent
      .childDirectory('templates')
      .childDirectory(name);
}

// Runs 'pub get' for the given path to ensure that .packages is created and
// all dependencies are present.
Future<void> _ensurePackageDependencies(String packagePath) async {
  await pub.get(
    context: PubContext.pubGet,
    directory: packagePath,
  );
274
}