mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00

* Remove superfluous words from comments. * Revert `win32_window.h` Which seems to come from `gallery`
378 lines
15 KiB
Dart
378 lines
15 KiB
Dart
// Copyright 2014 The Flutter Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
import 'package:file/file.dart';
|
|
import 'package:package_config/package_config.dart';
|
|
import 'package:package_config/package_config_types.dart';
|
|
|
|
import 'base/common.dart';
|
|
import 'base/file_system.dart';
|
|
import 'base/logger.dart';
|
|
import 'base/template.dart';
|
|
import 'cache.dart';
|
|
import 'dart/package_map.dart';
|
|
|
|
/// The Kotlin keywords which are not Java keywords.
|
|
/// They are escaped in Kotlin files.
|
|
///
|
|
/// https://kotlinlang.org/docs/keyword-reference.html
|
|
const List<String> kReservedKotlinKeywords = <String>['when', 'in', 'is'];
|
|
|
|
/// Expands templates in a directory to a destination. All files that must
|
|
/// 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
|
|
/// 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
|
|
/// 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'
|
|
/// extension may be used. Furthermore, templates may contain additional
|
|
/// test files intended to run on the CI. Test files must end in `.test.tmpl`
|
|
/// and are only included when the --implementation-tests flag is enabled.
|
|
///
|
|
/// Folders with platform/language-specific content must be named
|
|
/// '<platform>-<language>.tmpl'.
|
|
///
|
|
/// Files in the destination will contain none of the '.tmpl', '.copy.tmpl',
|
|
/// 'img.tmpl', or '-<language>.tmpl' extensions.
|
|
class Template {
|
|
factory Template(Directory templateSource, Directory? imageSourceDir, {
|
|
required FileSystem fileSystem,
|
|
required Logger logger,
|
|
required TemplateRenderer templateRenderer,
|
|
Set<Uri>? templateManifest,
|
|
}) {
|
|
return Template._(
|
|
<Directory>[templateSource],
|
|
imageSourceDir != null ? <Directory>[imageSourceDir] : <Directory>[],
|
|
fileSystem: fileSystem,
|
|
logger: logger,
|
|
templateRenderer: templateRenderer,
|
|
templateManifest: templateManifest,
|
|
);
|
|
}
|
|
|
|
Template._(
|
|
List<Directory> templateSources, this.imageSourceDirectories, {
|
|
required FileSystem fileSystem,
|
|
required Logger logger,
|
|
required TemplateRenderer templateRenderer,
|
|
required Set<Uri>? templateManifest,
|
|
}) : _fileSystem = fileSystem,
|
|
_logger = logger,
|
|
_templateRenderer = templateRenderer,
|
|
_templateManifest = templateManifest ?? <Uri>{} {
|
|
for (final Directory sourceDirectory in templateSources) {
|
|
if (!sourceDirectory.existsSync()) {
|
|
throwToolExit('Template source directory does not exist: ${sourceDirectory.absolute.path}');
|
|
}
|
|
}
|
|
|
|
final Map<FileSystemEntity, Directory> templateFiles = <FileSystemEntity, Directory>{
|
|
for (final Directory sourceDirectory in templateSources)
|
|
for (final FileSystemEntity entity in sourceDirectory.listSync(recursive: true))
|
|
entity: sourceDirectory,
|
|
};
|
|
for (final FileSystemEntity entity in templateFiles.keys.whereType<File>()) {
|
|
if (_templateManifest.isNotEmpty && !_templateManifest.contains(Uri.file(entity.absolute.path))) {
|
|
_logger.printTrace('Skipping ${entity.absolute.path}, missing from the template manifest.');
|
|
// Skip stale files in the flutter_tools directory.
|
|
continue;
|
|
}
|
|
|
|
final String relativePath = fileSystem.path.relative(entity.path,
|
|
from: templateFiles[entity]!.absolute.path);
|
|
if (relativePath.contains(templateExtension)) {
|
|
// If '.tmpl' appears anywhere within the path of this entity, it is
|
|
// a candidate for rendering. This catches cases where the folder
|
|
// itself is a template.
|
|
_templateFilePaths[relativePath] = fileSystem.path.absolute(entity.path);
|
|
}
|
|
}
|
|
}
|
|
|
|
static Future<Template> fromName(String name, {
|
|
required FileSystem fileSystem,
|
|
required Set<Uri>? templateManifest,
|
|
required Logger logger,
|
|
required TemplateRenderer templateRenderer,
|
|
}) async {
|
|
// All named templates are placed in the 'templates' directory
|
|
final Directory templateDir = _templateDirectoryInPackage(name, fileSystem);
|
|
final Directory imageDir = await templateImageDirectory(name, fileSystem, logger);
|
|
return Template._(
|
|
<Directory>[templateDir],
|
|
<Directory>[imageDir],
|
|
fileSystem: fileSystem,
|
|
logger: logger,
|
|
templateRenderer: templateRenderer,
|
|
templateManifest: templateManifest,
|
|
);
|
|
}
|
|
|
|
static Future<Template> merged(List<String> names, Directory directory, {
|
|
required FileSystem fileSystem,
|
|
required Set<Uri> templateManifest,
|
|
required Logger logger,
|
|
required TemplateRenderer templateRenderer,
|
|
}) async {
|
|
// All named templates are placed in the 'templates' directory
|
|
return Template._(
|
|
<Directory>[
|
|
for (final String name in names)
|
|
_templateDirectoryInPackage(name, fileSystem),
|
|
],
|
|
<Directory>[
|
|
for (final String name in names)
|
|
if ((await templateImageDirectory(name, fileSystem, logger)).existsSync())
|
|
await templateImageDirectory(name, fileSystem, logger),
|
|
],
|
|
fileSystem: fileSystem,
|
|
logger: logger,
|
|
templateRenderer: templateRenderer,
|
|
templateManifest: templateManifest,
|
|
);
|
|
}
|
|
|
|
final FileSystem _fileSystem;
|
|
final Logger _logger;
|
|
final Set<Uri> _templateManifest;
|
|
final TemplateRenderer _templateRenderer;
|
|
|
|
static const String templateExtension = '.tmpl';
|
|
static const String copyTemplateExtension = '.copy.tmpl';
|
|
static const String imageTemplateExtension = '.img.tmpl';
|
|
static const String testTemplateExtension = '.test.tmpl';
|
|
final Pattern _kTemplateLanguageVariant = RegExp(r'(\w+)-(\w+)\.tmpl.*');
|
|
final List<Directory> imageSourceDirectories;
|
|
|
|
final Map<String /* relative */, String /* absolute source */> _templateFilePaths = <String, String>{};
|
|
|
|
/// Render the template into [directory].
|
|
///
|
|
/// May throw a [ToolExit] if the directory is not writable.
|
|
int render(
|
|
Directory destination,
|
|
Map<String, Object?> context, {
|
|
bool overwriteExisting = true,
|
|
bool printStatusWhenWriting = true,
|
|
}) {
|
|
try {
|
|
destination.createSync(recursive: true);
|
|
} on FileSystemException catch (err) {
|
|
_logger.printError(err.toString());
|
|
throwToolExit('Failed to flutter create at ${destination.path}.');
|
|
}
|
|
int fileCount = 0;
|
|
final bool implementationTests = (context['implementationTests'] as bool?) ?? false;
|
|
|
|
/// 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)!;
|
|
final String? language = context['${platform}Language'] as String?;
|
|
if (language != match.group(2)) {
|
|
return null;
|
|
}
|
|
relativeDestinationPath = relativeDestinationPath.replaceAll('$platform-$language.tmpl', platform);
|
|
}
|
|
|
|
final bool android = (context['android'] as bool?) ?? false;
|
|
if (relativeDestinationPath.contains('android') && !android) {
|
|
return null;
|
|
}
|
|
|
|
final bool ios = (context['ios'] as bool?) ?? false;
|
|
if (relativeDestinationPath.contains('ios') && !ios) {
|
|
return null;
|
|
}
|
|
|
|
// Only build a web project if explicitly asked.
|
|
final bool web = (context['web'] as bool?) ?? false;
|
|
if (relativeDestinationPath.contains('web') && !web) {
|
|
return null;
|
|
}
|
|
// Only build a Linux project if explicitly asked.
|
|
final bool linux = (context['linux'] as bool?) ?? false;
|
|
if (relativeDestinationPath.startsWith('linux.tmpl') && !linux) {
|
|
return null;
|
|
}
|
|
// Only build a macOS project if explicitly asked.
|
|
final bool macOS = (context['macos'] as bool?) ?? false;
|
|
if (relativeDestinationPath.startsWith('macos.tmpl') && !macOS) {
|
|
return null;
|
|
}
|
|
// Only build a Windows project if explicitly asked.
|
|
final bool windows = (context['windows'] as bool?) ?? false;
|
|
if (relativeDestinationPath.startsWith('windows.tmpl') && !windows) {
|
|
return null;
|
|
}
|
|
|
|
final String? projectName = context['projectName'] as String?;
|
|
final String? androidIdentifier = context['androidIdentifier'] as String?;
|
|
final String? pluginClass = context['pluginClass'] as String?;
|
|
final String? pluginClassSnakeCase = context['pluginClassSnakeCase'] as String?;
|
|
final String destinationDirPath = destination.absolute.path;
|
|
final String pathSeparator = _fileSystem.path.separator;
|
|
String finalDestinationPath = _fileSystem.path
|
|
.join(destinationDirPath, relativeDestinationPath)
|
|
.replaceAll(copyTemplateExtension, '')
|
|
.replaceAll(imageTemplateExtension, '')
|
|
.replaceAll(testTemplateExtension, '')
|
|
.replaceAll(templateExtension, '');
|
|
|
|
if (android && androidIdentifier != null) {
|
|
finalDestinationPath = finalDestinationPath
|
|
.replaceAll('androidIdentifier', androidIdentifier.replaceAll('.', pathSeparator));
|
|
}
|
|
if (projectName != null) {
|
|
finalDestinationPath = finalDestinationPath.replaceAll('projectName', projectName);
|
|
}
|
|
// This must be before the pluginClass replacement step.
|
|
if (pluginClassSnakeCase != null) {
|
|
finalDestinationPath = finalDestinationPath.replaceAll('pluginClassSnakeCase', pluginClassSnakeCase);
|
|
}
|
|
if (pluginClass != null) {
|
|
finalDestinationPath = finalDestinationPath.replaceAll('pluginClass', pluginClass);
|
|
}
|
|
return finalDestinationPath;
|
|
}
|
|
|
|
_templateFilePaths.forEach((String relativeDestinationPath, String absoluteSourcePath) {
|
|
final bool withRootModule = context['withRootModule'] as bool? ?? false;
|
|
if (!withRootModule && absoluteSourcePath.contains('flutter_root')) {
|
|
return;
|
|
}
|
|
|
|
if (!implementationTests && absoluteSourcePath.contains(testTemplateExtension)) {
|
|
return;
|
|
}
|
|
|
|
final String? finalDestinationPath = renderPath(relativeDestinationPath);
|
|
if (finalDestinationPath == null) {
|
|
return;
|
|
}
|
|
final File finalDestinationFile = _fileSystem.file(finalDestinationPath);
|
|
final String relativePathForLogging = _fileSystem.path.relative(finalDestinationFile.path);
|
|
|
|
// Step 1: Check if the file needs to be overwritten.
|
|
|
|
if (finalDestinationFile.existsSync()) {
|
|
if (overwriteExisting) {
|
|
finalDestinationFile.deleteSync(recursive: true);
|
|
if (printStatusWhenWriting) {
|
|
_logger.printStatus(' $relativePathForLogging (overwritten)');
|
|
}
|
|
} else {
|
|
// The file exists but we cannot overwrite it, move on.
|
|
if (printStatusWhenWriting) {
|
|
_logger.printTrace(' $relativePathForLogging (existing - skipped)');
|
|
}
|
|
return;
|
|
}
|
|
} else {
|
|
if (printStatusWhenWriting) {
|
|
_logger.printStatus(' $relativePathForLogging (created)');
|
|
}
|
|
}
|
|
|
|
fileCount += 1;
|
|
|
|
finalDestinationFile.createSync(recursive: true);
|
|
final File sourceFile = _fileSystem.file(absoluteSourcePath);
|
|
|
|
// Step 2: If the absolute paths ends with a '.copy.tmpl', this file does
|
|
// not need mustache rendering but needs to be directly copied.
|
|
|
|
if (sourceFile.path.endsWith(copyTemplateExtension)) {
|
|
sourceFile.copySync(finalDestinationFile.path);
|
|
|
|
return;
|
|
}
|
|
|
|
// 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 List<File> potentials = <File>[
|
|
for (final Directory imageSourceDir in imageSourceDirectories)
|
|
_fileSystem.file(_fileSystem.path
|
|
.join(imageSourceDir.path, relativeDestinationPath.replaceAll(imageTemplateExtension, ''))),
|
|
];
|
|
|
|
if (potentials.any((File file) => file.existsSync())) {
|
|
final File imageSourceFile = potentials.firstWhere((File file) => file.existsSync());
|
|
|
|
imageSourceFile.copySync(finalDestinationFile.path);
|
|
} else {
|
|
throwToolExit('Image File not found ${finalDestinationFile.path}');
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// Step 4: If the absolute path ends with a '.tmpl', this file needs
|
|
// rendering via mustache.
|
|
|
|
if (sourceFile.path.endsWith(templateExtension)) {
|
|
final String templateContents = sourceFile.readAsStringSync();
|
|
final String? androidIdentifier = context['androidIdentifier'] as String?;
|
|
if (finalDestinationFile.path.endsWith('.kt') && androidIdentifier != null) {
|
|
context['androidIdentifier'] = _escapeKotlinKeywords(androidIdentifier);
|
|
}
|
|
|
|
final String renderedContents = _templateRenderer.renderString(templateContents, context);
|
|
|
|
finalDestinationFile.writeAsStringSync(renderedContents);
|
|
|
|
return;
|
|
}
|
|
|
|
// Step 5: This file does not end in .tmpl but is in a directory that
|
|
// does. Directly copy the file to the destination.
|
|
sourceFile.copySync(finalDestinationFile.path);
|
|
});
|
|
|
|
return fileCount;
|
|
}
|
|
}
|
|
|
|
Directory _templateDirectoryInPackage(String name, FileSystem fileSystem) {
|
|
final String templatesDir = fileSystem.path.join(Cache.flutterRoot!,
|
|
'packages', 'flutter_tools', 'templates');
|
|
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.
|
|
/// if 'name' is null, return the parent template directory.
|
|
Future<Directory> templateImageDirectory(String? name, FileSystem fileSystem, Logger logger) async {
|
|
final String toolPackagePath = fileSystem.path.join(
|
|
Cache.flutterRoot!, 'packages', 'flutter_tools');
|
|
final String packageFilePath = fileSystem.path.join(toolPackagePath, '.dart_tool', 'package_config.json');
|
|
final PackageConfig packageConfig = await loadPackageConfigWithLogging(
|
|
fileSystem.file(packageFilePath),
|
|
logger: logger,
|
|
);
|
|
final Uri? imagePackageLibDir = packageConfig['flutter_template_images']?.packageUriRoot;
|
|
final Directory templateDirectory = fileSystem.directory(imagePackageLibDir)
|
|
.parent
|
|
.childDirectory('templates');
|
|
return name == null ? templateDirectory : templateDirectory.childDirectory(name);
|
|
}
|
|
|
|
String _escapeKotlinKeywords(String androidIdentifier) {
|
|
final List<String> segments = androidIdentifier.split('.');
|
|
final List<String> correctedSegments = segments.map(
|
|
(String segment) => kReservedKotlinKeywords.contains(segment) ? '`$segment`' : segment
|
|
).toList();
|
|
return correctedSegments.join('.');
|
|
}
|