blob: 07997404da202cc71176fd0e1fce19e38a48f5b9 [file] [log] [blame]
// 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:xml/xml.dart';
import '../base/deferred_component.dart';
import '../base/error_handling_io.dart';
import '../base/file_system.dart';
import '../base/logger.dart';
import '../globals.dart' as globals;
import '../project.dart';
import '../template.dart';
import 'deferred_components_validator.dart';
/// A class to configure and run deferred component setup verification checks
/// and tasks.
///
/// Once constructed, checks and tasks can be executed by calling the respective
/// methods. The results of the checks are stored internally and can be
/// displayed to the user by calling [displayResults].
class DeferredComponentsPrebuildValidator extends DeferredComponentsValidator {
/// Constructs a validator instance.
///
/// The [templatesDir] parameter is optional. If null, the tool's default
/// templates directory will be used.
///
/// When [exitOnFail] is set to true, the [handleResults] and [attemptToolExit]
/// methods will exit the tool when this validator detects a recommended
/// change. This defaults to true.
DeferredComponentsPrebuildValidator(super.projectDir, super.logger, super.platform, {
super.exitOnFail,
super.title,
Directory? templatesDir,
}) : _templatesDir = templatesDir;
final Directory? _templatesDir;
/// Checks if an android dynamic feature module exists for each deferred
/// component.
///
/// Returns true if the check passed with no recommended changes, and false
/// otherwise.
///
/// This method looks for the existence of `android/<componentname>/build.gradle`
/// and `android/<componentname>/src/main/AndroidManifest.xml`. If either of
/// these files does not exist, it will generate it in the validator output
/// directory based off of a template.
///
/// This method does not check if the contents of either of the files are
/// valid, as there are many ways that they can be validly configured.
Future<bool> checkAndroidDynamicFeature(List<DeferredComponent> components) async {
inputs.add(projectDir.childFile('pubspec.yaml'));
if (components.isEmpty) {
return false;
}
bool changesMade = false;
for (final DeferredComponent component in components) {
final _DeferredComponentAndroidFiles androidFiles = _DeferredComponentAndroidFiles(
name: component.name,
projectDir: projectDir,
logger: logger,
templatesDir: _templatesDir
);
if (!androidFiles.verifyFilesExist()) {
// generate into temp directory
final Map<String, List<File>> results = await androidFiles.generateFiles(
alternateAndroidDir: outputDir,
clearAlternateOutputDir: true,
);
if (results.containsKey('outputs')) {
for (final File file in results['outputs']!) {
generatedFiles.add(file.path);
changesMade = true;
}
outputs.addAll(results['outputs']!);
}
if (results.containsKey('inputs')) {
inputs.addAll(results['inputs']!);
}
}
}
return !changesMade;
}
/// Checks if the base module `app`'s `strings.xml` contain string
/// resources for each component's name.
///
/// Returns true if the check passed with no recommended changes, and false
/// otherwise.
///
/// In each dynamic feature module's AndroidManifest.xml, the
/// name of the module is a string resource. This checks if
/// the needed string resources are in the base module `strings.xml`.
/// If not, this method will generate a modified `strings.xml` (or a
/// completely new one if the original file did not exist) in the
/// validator's output directory.
///
/// For example, if there is a deferred component named `component1`,
/// there should be the following string resource:
///
/// <string name="component1Name">component1</string>
///
/// The string element's name attribute should be the component name with
/// `Name` as a suffix, and the text contents should be the component name.
bool checkAndroidResourcesStrings(List<DeferredComponent> components) {
final Directory androidDir = projectDir.childDirectory('android');
inputs.add(projectDir.childFile('pubspec.yaml'));
// Add component name mapping to strings.xml
final File stringRes = androidDir
.childDirectory('app')
.childDirectory('src')
.childDirectory('main')
.childDirectory('res')
.childDirectory('values')
.childFile('strings.xml');
inputs.add(stringRes);
final File stringResOutput = outputDir
.childDirectory('app')
.childDirectory('src')
.childDirectory('main')
.childDirectory('res')
.childDirectory('values')
.childFile('strings.xml');
ErrorHandlingFileSystem.deleteIfExists(stringResOutput);
if (components.isEmpty) {
return true;
}
final Map<String, String> requiredEntriesMap = <String, String>{};
for (final DeferredComponent component in components) {
requiredEntriesMap['${component.name}Name'] = component.name;
}
if (stringRes.existsSync()) {
bool modified = false;
XmlDocument document;
try {
document = XmlDocument.parse(stringRes.readAsStringSync());
} on XmlException {
invalidFiles[stringRes.path] = 'Error parsing $stringRes '
'Please ensure that the strings.xml is a valid XML document and '
'try again.';
return false;
}
// Check if all required lines are present, and fix if name exists, but
// wrong string stored.
for (final XmlElement resources in document.findAllElements('resources')) {
for (final XmlElement element in resources.findElements('string')) {
final String? name = element.getAttribute('name');
if (requiredEntriesMap.containsKey(name)) {
if (element.innerText != requiredEntriesMap[name]) {
element.innerText = requiredEntriesMap[name]!;
modified = true;
}
requiredEntriesMap.remove(name);
}
}
requiredEntriesMap.forEach((String key, String value) {
modified = true;
final XmlElement newStringElement = XmlElement(
XmlName.fromString('string'),
<XmlAttribute>[
XmlAttribute(XmlName.fromString('name'), key),
],
<XmlNode>[
XmlText(value),
],
);
resources.children.add(newStringElement);
});
break;
}
if (modified) {
stringResOutput.createSync(recursive: true);
stringResOutput.writeAsStringSync(document.toXmlString(pretty: true));
modifiedFiles.add(stringResOutput.path);
return false;
}
return true;
}
// strings.xml does not exist, generate completely new file.
stringResOutput.createSync(recursive: true);
final StringBuffer buffer = StringBuffer();
buffer.writeln('''
<?xml version="1.0" encoding="utf-8"?>
<resources>
''');
for (final String key in requiredEntriesMap.keys) {
buffer.write(' <string name="$key">${requiredEntriesMap[key]}</string>\n');
}
buffer.write(
'''
</resources>
''');
stringResOutput.writeAsStringSync(buffer.toString(), flush: true, mode: FileMode.append);
generatedFiles.add(stringResOutput.path);
return false;
}
/// Deletes all files inside of the validator's output directory.
void clearOutputDir() {
final Directory dir = projectDir.childDirectory('build').childDirectory(DeferredComponentsValidator.kDeferredComponentsTempDirectory);
ErrorHandlingFileSystem.deleteIfExists(dir, recursive: true);
}
}
// Handles a single deferred component's android dynamic feature module
// directory.
class _DeferredComponentAndroidFiles {
_DeferredComponentAndroidFiles({
required this.name,
required this.projectDir,
required this.logger,
Directory? templatesDir,
}) : _templatesDir = templatesDir;
// The name of the deferred component.
final String name;
final Directory projectDir;
final Logger logger;
final Directory? _templatesDir;
Directory get androidDir => projectDir.childDirectory('android');
Directory get componentDir => androidDir.childDirectory(name);
File get androidManifestFile => componentDir.childDirectory('src').childDirectory('main').childFile('AndroidManifest.xml');
File get buildGradleFile {
if (componentDir.childFile('build.gradle').existsSync()) {
return componentDir.childFile('build.gradle');
}
return componentDir.childFile('build.gradle.kts');
}
// True when AndroidManifest.xml and build.gradle/build.gradle.kts exist for
// the android dynamic feature.
bool verifyFilesExist() {
return androidManifestFile.existsSync() && buildGradleFile.existsSync();
}
// Generates any missing basic files for the dynamic feature into a temporary directory.
Future<Map<String, List<File>>> generateFiles({Directory? alternateAndroidDir, bool clearAlternateOutputDir = false}) async {
final Directory outputDir = alternateAndroidDir?.childDirectory(name) ?? componentDir;
if (clearAlternateOutputDir && alternateAndroidDir != null) {
ErrorHandlingFileSystem.deleteIfExists(outputDir);
}
final List<File> inputs = <File>[];
inputs.add(androidManifestFile);
inputs.add(buildGradleFile);
final Map<String, List<File>> results = <String, List<File>>{'inputs': inputs};
results['outputs'] = await _setupComponentFiles(outputDir);
return results;
}
// generates default build.gradle and AndroidManifest.xml for the deferred component.
Future<List<File>> _setupComponentFiles(Directory outputDir) async {
Template template;
final Directory? templatesDir = _templatesDir;
if (templatesDir != null) {
final Directory templateComponentDir = templatesDir.childDirectory('module${globals.fs.path.separator}android${globals.fs.path.separator}deferred_component');
template = Template(templateComponentDir, templateComponentDir,
fileSystem: globals.fs,
logger: logger,
templateRenderer: globals.templateRenderer,
);
} else {
template = await Template.fromName('module${globals.fs.path.separator}android${globals.fs.path.separator}deferred_component',
fileSystem: globals.fs,
templateManifest: null,
logger: logger,
templateRenderer: globals.templateRenderer,
);
}
final Map<String, Object> context = <String, Object>{
'androidIdentifier': FlutterProject.current().manifest.androidPackage ?? 'com.example.${FlutterProject.current().manifest.appName}',
'componentName': name,
};
template.render(outputDir, context);
final List<File> generatedFiles = <File>[];
final File tempBuildGradle = outputDir.childFile('build.gradle');
if (!buildGradleFile.existsSync()) {
generatedFiles.add(tempBuildGradle);
} else {
ErrorHandlingFileSystem.deleteIfExists(tempBuildGradle);
}
final File tempAndroidManifest = outputDir
.childDirectory('src')
.childDirectory('main')
.childFile('AndroidManifest.xml');
if (!androidManifestFile.existsSync()) {
generatedFiles.add(tempAndroidManifest);
} else {
ErrorHandlingFileSystem.deleteIfExists(tempAndroidManifest);
}
return generatedFiles;
}
}