1// Copyright 2016 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5import 'package:mustache/mustache.dart' as mustache; 6 7import 'base/common.dart'; 8import 'base/file_system.dart'; 9import 'cache.dart'; 10import 'globals.dart'; 11 12/// Expands templates in a directory to a destination. All files that must 13/// undergo template expansion should end with the '.tmpl' extension. All other 14/// files are ignored. In case the contents of entire directories must be copied 15/// as is, the directory itself can end with '.tmpl' extension. Files within 16/// such a directory may also contain the '.tmpl' extension and will be 17/// considered for expansion. In case certain files need to be copied but 18/// without template expansion (images, data files, etc.), the '.copy.tmpl' 19/// extension may be used. 20/// 21/// Folders with platform/language-specific content must be named 22/// '<platform>-<language>.tmpl'. 23/// 24/// Files in the destination will contain none of the '.tmpl', '.copy.tmpl' 25/// or '-<language>.tmpl' extensions. 26class Template { 27 Template(Directory templateSource, Directory baseDir) { 28 _templateFilePaths = <String, String>{}; 29 30 if (!templateSource.existsSync()) { 31 return; 32 } 33 34 final List<FileSystemEntity> templateFiles = templateSource.listSync(recursive: true); 35 36 for (FileSystemEntity entity in templateFiles) { 37 if (entity is! File) { 38 // We are only interesting in template *file* URIs. 39 continue; 40 } 41 42 final String relativePath = fs.path.relative(entity.path, 43 from: baseDir.absolute.path); 44 45 if (relativePath.contains(templateExtension)) { 46 // If '.tmpl' appears anywhere within the path of this entity, it is 47 // is a candidate for rendering. This catches cases where the folder 48 // itself is a template. 49 _templateFilePaths[relativePath] = fs.path.absolute(entity.path); 50 } 51 } 52 } 53 54 factory Template.fromName(String name) { 55 // All named templates are placed in the 'templates' directory 56 final Directory templateDir = templateDirectoryInPackage(name); 57 return Template(templateDir, templateDir); 58 } 59 60 static const String templateExtension = '.tmpl'; 61 static const String copyTemplateExtension = '.copy.tmpl'; 62 final Pattern _kTemplateLanguageVariant = RegExp(r'(\w+)-(\w+)\.tmpl.*'); 63 64 Map<String /* relative */, String /* absolute source */> _templateFilePaths; 65 66 /// Render the template into [directory]. 67 /// 68 /// May throw a [ToolExit] if the directory is not writable. 69 int render( 70 Directory destination, 71 Map<String, dynamic> context, { 72 bool overwriteExisting = true, 73 bool printStatusWhenWriting = true, 74 }) { 75 try { 76 destination.createSync(recursive: true); 77 } on FileSystemException catch (err) { 78 printError(err.toString()); 79 throwToolExit('Failed to flutter create at ${destination.path}.'); 80 return 0; 81 } 82 int fileCount = 0; 83 84 /// Returns the resolved destination path corresponding to the specified 85 /// raw destination path, after performing language filtering and template 86 /// expansion on the path itself. 87 /// 88 /// Returns null if the given raw destination path has been filtered. 89 String renderPath(String relativeDestinationPath) { 90 final Match match = _kTemplateLanguageVariant.matchAsPrefix(relativeDestinationPath); 91 if (match != null) { 92 final String platform = match.group(1); 93 final String language = context['${platform}Language']; 94 if (language != match.group(2)) 95 return null; 96 relativeDestinationPath = relativeDestinationPath.replaceAll('$platform-$language.tmpl', platform); 97 } 98 // Only build a web project if explicitly asked. 99 final bool web = context['web']; 100 if (relativeDestinationPath.contains('web') && !web) { 101 return null; 102 } 103 final String projectName = context['projectName']; 104 final String androidIdentifier = context['androidIdentifier']; 105 final String pluginClass = context['pluginClass']; 106 final String destinationDirPath = destination.absolute.path; 107 final String pathSeparator = fs.path.separator; 108 String finalDestinationPath = fs.path 109 .join(destinationDirPath, relativeDestinationPath) 110 .replaceAll(copyTemplateExtension, '') 111 .replaceAll(templateExtension, ''); 112 113 if (androidIdentifier != null) { 114 finalDestinationPath = finalDestinationPath 115 .replaceAll('androidIdentifier', androidIdentifier.replaceAll('.', pathSeparator)); 116 } 117 if (projectName != null) 118 finalDestinationPath = finalDestinationPath.replaceAll('projectName', projectName); 119 if (pluginClass != null) 120 finalDestinationPath = finalDestinationPath.replaceAll('pluginClass', pluginClass); 121 return finalDestinationPath; 122 } 123 124 _templateFilePaths.forEach((String relativeDestinationPath, String absoluteSourcePath) { 125 final bool withRootModule = context['withRootModule'] ?? false; 126 if (!withRootModule && absoluteSourcePath.contains('flutter_root')) 127 return; 128 129 final String finalDestinationPath = renderPath(relativeDestinationPath); 130 if (finalDestinationPath == null) 131 return; 132 final File finalDestinationFile = fs.file(finalDestinationPath); 133 final String relativePathForLogging = fs.path.relative(finalDestinationFile.path); 134 135 // Step 1: Check if the file needs to be overwritten. 136 137 if (finalDestinationFile.existsSync()) { 138 if (overwriteExisting) { 139 finalDestinationFile.deleteSync(recursive: true); 140 if (printStatusWhenWriting) 141 printStatus(' $relativePathForLogging (overwritten)'); 142 } else { 143 // The file exists but we cannot overwrite it, move on. 144 if (printStatusWhenWriting) 145 printTrace(' $relativePathForLogging (existing - skipped)'); 146 return; 147 } 148 } else { 149 if (printStatusWhenWriting) 150 printStatus(' $relativePathForLogging (created)'); 151 } 152 153 fileCount++; 154 155 finalDestinationFile.createSync(recursive: true); 156 final File sourceFile = fs.file(absoluteSourcePath); 157 158 // Step 2: If the absolute paths ends with a '.copy.tmpl', this file does 159 // not need mustache rendering but needs to be directly copied. 160 161 if (sourceFile.path.endsWith(copyTemplateExtension)) { 162 sourceFile.copySync(finalDestinationFile.path); 163 164 return; 165 } 166 167 // Step 3: If the absolute path ends with a '.tmpl', this file needs 168 // rendering via mustache. 169 170 if (sourceFile.path.endsWith(templateExtension)) { 171 final String templateContents = sourceFile.readAsStringSync(); 172 final String renderedContents = mustache.Template(templateContents).renderString(context); 173 174 finalDestinationFile.writeAsStringSync(renderedContents); 175 176 return; 177 } 178 179 // Step 4: This file does not end in .tmpl but is in a directory that 180 // does. Directly copy the file to the destination. 181 182 sourceFile.copySync(finalDestinationFile.path); 183 }); 184 185 return fileCount; 186 } 187} 188 189Directory templateDirectoryInPackage(String name) { 190 final String templatesDir = fs.path.join(Cache.flutterRoot, 191 'packages', 'flutter_tools', 'templates'); 192 return fs.directory(fs.path.join(templatesDir, name)); 193} 194