mirror of
https://github.com/flutter/flutter
synced 2024-10-13 11:42:54 +00:00
[flutter_tools] reland: integrate l10n tool into hot reload/restart/build (#57510)
Reland: #56167
This commit is contained in:
parent
27a6705aa4
commit
70b889a9a3
|
@ -88,6 +88,12 @@ class CommandHelp {
|
|||
'Detach (terminate "flutter run" but leave application running).',
|
||||
);
|
||||
|
||||
CommandHelpOption _g;
|
||||
CommandHelpOption get g => _g ??= _makeOption(
|
||||
'g',
|
||||
'Run source code generators.'
|
||||
);
|
||||
|
||||
CommandHelpOption _h;
|
||||
CommandHelpOption get h => _h ??= _makeOption(
|
||||
'h',
|
||||
|
|
|
@ -487,6 +487,7 @@ class _ResidentWebRunner extends ResidentWebRunner {
|
|||
);
|
||||
|
||||
if (debuggingOptions.buildInfo.isDebug) {
|
||||
await runSourceGenerators();
|
||||
// Full restart is always false for web, since the extra recompile is wasteful.
|
||||
final UpdateFSReport report = await _updateDevFS(fullRestart: false);
|
||||
if (report.success) {
|
||||
|
|
|
@ -134,6 +134,12 @@ abstract class Target {
|
|||
/// A list of zero or more depfiles, located directly under {BUILD_DIR}.
|
||||
List<String> get depfiles => const <String>[];
|
||||
|
||||
/// Whether this target can be executed with the given [environment].
|
||||
///
|
||||
/// Returning `true` will cause [build] to be skipped. This is equivalent
|
||||
/// to a build that produces no outputs.
|
||||
bool canSkip(Environment environment) => false;
|
||||
|
||||
/// The action which performs this build step.
|
||||
Future<void> build(Environment environment);
|
||||
|
||||
|
@ -773,18 +779,26 @@ class _BuildInstance {
|
|||
updateGraph();
|
||||
return succeeded;
|
||||
}
|
||||
logger.printTrace('${node.target.name}: Starting due to ${node.invalidatedReasons}');
|
||||
await node.target.build(environment);
|
||||
logger.printTrace('${node.target.name}: Complete');
|
||||
// Clear old inputs. These will be replaced with new inputs/outputs
|
||||
// after the target is run. In the case of a runtime skip, each list
|
||||
// must be empty to ensure the previous outputs are purged.
|
||||
node.inputs.clear();
|
||||
node.outputs.clear();
|
||||
|
||||
node.inputs
|
||||
..clear()
|
||||
..addAll(node.target.resolveInputs(environment).sources);
|
||||
node.outputs
|
||||
..clear()
|
||||
..addAll(node.target.resolveOutputs(environment).sources);
|
||||
// Check if we can skip via runtime dependencies.
|
||||
final bool runtimeSkip = node.target.canSkip(environment);
|
||||
if (runtimeSkip) {
|
||||
logger.printTrace('Skipping target: ${node.target.name}');
|
||||
skipped = true;
|
||||
} else {
|
||||
logger.printTrace('${node.target.name}: Starting due to ${node.invalidatedReasons}');
|
||||
await node.target.build(environment);
|
||||
logger.printTrace('${node.target.name}: Complete');
|
||||
node.inputs.addAll(node.target.resolveInputs(environment).sources);
|
||||
node.outputs.addAll(node.target.resolveOutputs(environment).sources);
|
||||
}
|
||||
|
||||
// If we were missing the depfile, resolve input files after executing the
|
||||
// If we were missing the depfile, resolve input files after executing the
|
||||
// target so that all file hashes are up to date on the next run.
|
||||
if (node.missingDepfile) {
|
||||
await fileCache.diffFileList(node.inputs);
|
||||
|
|
|
@ -17,6 +17,7 @@ import '../depfile.dart';
|
|||
import '../exceptions.dart';
|
||||
import 'assets.dart';
|
||||
import 'icon_tree_shaker.dart';
|
||||
import 'localizations.dart';
|
||||
|
||||
/// The define to pass a [BuildMode].
|
||||
const String kBuildMode = 'BuildMode';
|
||||
|
@ -183,7 +184,9 @@ class KernelSnapshot extends Target {
|
|||
];
|
||||
|
||||
@override
|
||||
List<Target> get dependencies => <Target>[];
|
||||
List<Target> get dependencies => const <Target>[
|
||||
GenerateLocalizationsTarget(),
|
||||
];
|
||||
|
||||
@override
|
||||
Future<void> build(Environment environment) async {
|
||||
|
|
|
@ -0,0 +1,272 @@
|
|||
// 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:meta/meta.dart';
|
||||
import 'package:process/process.dart';
|
||||
import 'package:yaml/yaml.dart';
|
||||
|
||||
import '../../artifacts.dart';
|
||||
import '../../base/file_system.dart';
|
||||
import '../../base/io.dart';
|
||||
import '../../base/logger.dart';
|
||||
import '../../convert.dart';
|
||||
import '../../globals.dart' as globals;
|
||||
import '../build_system.dart';
|
||||
import '../depfile.dart';
|
||||
|
||||
const String _kDependenciesFileName = 'gen_l10n_inputs_and_outputs.json';
|
||||
|
||||
/// Run the localizations generation script with the configuration [options].
|
||||
Future<void> generateLocalizations({
|
||||
@required LocalizationOptions options,
|
||||
@required String flutterRoot,
|
||||
@required FileSystem fileSystem,
|
||||
@required ProcessManager processManager,
|
||||
@required Logger logger,
|
||||
@required Directory projectDir,
|
||||
@required String dartBinaryPath,
|
||||
@required Directory dependenciesDir,
|
||||
}) async {
|
||||
final String genL10nPath = fileSystem.path.join(
|
||||
flutterRoot,
|
||||
'dev',
|
||||
'tools',
|
||||
'localization',
|
||||
'bin',
|
||||
'gen_l10n.dart',
|
||||
);
|
||||
final ProcessResult result = await processManager.run(<String>[
|
||||
dartBinaryPath,
|
||||
genL10nPath,
|
||||
'--gen-inputs-and-outputs-list=${dependenciesDir.path}',
|
||||
if (options.arbDirectory != null)
|
||||
'--arb-dir=${options.arbDirectory.toFilePath()}',
|
||||
if (options.templateArbFile != null)
|
||||
'--template-arb-file=${options.templateArbFile.toFilePath()}',
|
||||
if (options.outputLocalizationsFile != null)
|
||||
'--output-localization-file=${options.outputLocalizationsFile.toFilePath()}',
|
||||
if (options.untranslatedMessagesFile != null)
|
||||
'--untranslated-messages-file=${options.untranslatedMessagesFile.toFilePath()}',
|
||||
if (options.outputClass != null)
|
||||
'--output-class=${options.outputClass}',
|
||||
if (options.headerFile != null)
|
||||
'--header-file=${options.headerFile.toFilePath()}',
|
||||
if (options.header != null)
|
||||
'--header=${options.header}',
|
||||
if (options.deferredLoading != null)
|
||||
'--use-deferred-loading',
|
||||
if (options.preferredSupportedLocales != null)
|
||||
'--preferred-supported-locales=${options.preferredSupportedLocales}',
|
||||
]);
|
||||
if (result.exitCode != 0) {
|
||||
logger.printError(result.stdout + result.stderr as String);
|
||||
throw Exception();
|
||||
}
|
||||
}
|
||||
|
||||
/// A build step that runs the generate localizations script from
|
||||
/// dev/tool/localizations.
|
||||
class GenerateLocalizationsTarget extends Target {
|
||||
const GenerateLocalizationsTarget();
|
||||
|
||||
@override
|
||||
List<Target> get dependencies => <Target>[];
|
||||
|
||||
@override
|
||||
List<Source> get inputs => <Source>[
|
||||
// This is added as a convenience for developing the tool.
|
||||
const Source.pattern('{FLUTTER_ROOT}/packages/flutter_tools/lib/src/build_system/targets/localizations.dart'),
|
||||
// TODO(jonahwilliams): once https://github.com/flutter/flutter/issues/56321 is
|
||||
// complete, we should add the artifact as a dependency here. Since the tool runs
|
||||
// this code from source, looking up each dependency will be cumbersome.
|
||||
];
|
||||
|
||||
@override
|
||||
String get name => 'gen_localizations';
|
||||
|
||||
@override
|
||||
List<Source> get outputs => <Source>[];
|
||||
|
||||
@override
|
||||
List<String> get depfiles => <String>['gen_localizations.d'];
|
||||
|
||||
@override
|
||||
bool canSkip(Environment environment) {
|
||||
final File configFile = environment.projectDir.childFile('l10n.yaml');
|
||||
return !configFile.existsSync();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> build(Environment environment) async {
|
||||
final File configFile = environment.projectDir.childFile('l10n.yaml');
|
||||
assert(configFile.existsSync());
|
||||
|
||||
final LocalizationOptions options = parseLocalizationsOptions(
|
||||
file: configFile,
|
||||
logger: globals.logger,
|
||||
);
|
||||
final DepfileService depfileService = DepfileService(
|
||||
logger: environment.logger,
|
||||
fileSystem: environment.fileSystem,
|
||||
);
|
||||
|
||||
await generateLocalizations(
|
||||
fileSystem: environment.fileSystem,
|
||||
flutterRoot: environment.flutterRootDir.path,
|
||||
logger: environment.logger,
|
||||
processManager: environment.processManager,
|
||||
options: options,
|
||||
projectDir: environment.projectDir,
|
||||
dartBinaryPath: environment.artifacts
|
||||
.getArtifactPath(Artifact.engineDartBinary),
|
||||
dependenciesDir: environment.buildDir,
|
||||
);
|
||||
final Map<String, Object> dependencies = json
|
||||
.decode(environment.buildDir.childFile(_kDependenciesFileName).readAsStringSync()) as Map<String, Object>;
|
||||
final Depfile depfile = Depfile(
|
||||
<File>[
|
||||
configFile,
|
||||
for (dynamic inputFile in dependencies['inputs'] as List<dynamic>)
|
||||
environment.fileSystem.file(inputFile)
|
||||
],
|
||||
<File>[
|
||||
for (dynamic outputFile in dependencies['outputs'] as List<dynamic>)
|
||||
environment.fileSystem.file(outputFile)
|
||||
]
|
||||
);
|
||||
depfileService.writeToFile(
|
||||
depfile,
|
||||
environment.buildDir.childFile('gen_localizations.d'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Typed configuration from the localizations config file.
|
||||
class LocalizationOptions {
|
||||
const LocalizationOptions({
|
||||
this.arbDirectory,
|
||||
this.templateArbFile,
|
||||
this.outputLocalizationsFile,
|
||||
this.untranslatedMessagesFile,
|
||||
this.header,
|
||||
this.outputClass,
|
||||
this.preferredSupportedLocales,
|
||||
this.headerFile,
|
||||
this.deferredLoading,
|
||||
});
|
||||
|
||||
/// The `--arb-dir` argument.
|
||||
///
|
||||
/// The directory where all localization files should reside.
|
||||
final Uri arbDirectory;
|
||||
|
||||
/// The `--template-arb-file` argument.
|
||||
///
|
||||
/// This URI is relative to [arbDirectory].
|
||||
final Uri templateArbFile;
|
||||
|
||||
/// The `--output-localization-file` argument.
|
||||
///
|
||||
/// This URI is relative to [arbDirectory].
|
||||
final Uri outputLocalizationsFile;
|
||||
|
||||
/// The `--untranslated-messages-file` argument.
|
||||
///
|
||||
/// This URI is relative to [arbDirectory].
|
||||
final Uri untranslatedMessagesFile;
|
||||
|
||||
/// The `--header` argument.
|
||||
///
|
||||
/// The header to prepend to the generated Dart localizations
|
||||
final String header;
|
||||
|
||||
/// The `--output-class` argument.
|
||||
final String outputClass;
|
||||
|
||||
/// The `--preferred-supported-locales` argument.
|
||||
final String preferredSupportedLocales;
|
||||
|
||||
/// The `--header-file` argument.
|
||||
///
|
||||
/// A file containing the header to preprend to the generated
|
||||
/// Dart localizations.
|
||||
final Uri headerFile;
|
||||
|
||||
/// The `--use-deferred-loading` argument.
|
||||
///
|
||||
/// Whether to generate the Dart localization file with locales imported
|
||||
/// as deferred.
|
||||
final bool deferredLoading;
|
||||
}
|
||||
|
||||
/// Parse the localizations configuration options from [file].
|
||||
///
|
||||
/// Throws [Exception] if any of the contents are invalid. Returns a
|
||||
/// [LocalizationOptions] with all fields as `null` if the config file exists
|
||||
/// but is empty.
|
||||
LocalizationOptions parseLocalizationsOptions({
|
||||
@required File file,
|
||||
@required Logger logger,
|
||||
}) {
|
||||
final String contents = file.readAsStringSync();
|
||||
if (contents.trim().isEmpty) {
|
||||
return const LocalizationOptions();
|
||||
}
|
||||
final YamlNode yamlNode = loadYamlNode(file.readAsStringSync());
|
||||
if (yamlNode is! YamlMap) {
|
||||
logger.printError('Expected ${file.path} to contain a map, instead was $yamlNode');
|
||||
throw Exception();
|
||||
}
|
||||
final YamlMap yamlMap = yamlNode as YamlMap;
|
||||
return LocalizationOptions(
|
||||
arbDirectory: _tryReadUri(yamlMap, 'arb-dir', logger),
|
||||
templateArbFile: _tryReadUri(yamlMap, 'template-arb-file', logger),
|
||||
outputLocalizationsFile: _tryReadUri(yamlMap, 'output-localization-file', logger),
|
||||
untranslatedMessagesFile: _tryReadUri(yamlMap, 'untranslated-messages-file', logger),
|
||||
header: _tryReadString(yamlMap, 'header', logger),
|
||||
outputClass: _tryReadString(yamlMap, 'output-class', logger),
|
||||
preferredSupportedLocales: _tryReadString(yamlMap, 'preferred-supported-locales', logger),
|
||||
headerFile: _tryReadUri(yamlMap, 'header-file', logger),
|
||||
deferredLoading: _tryReadBool(yamlMap, 'use-deferred-loading', logger),
|
||||
);
|
||||
}
|
||||
|
||||
// Try to read a `bool` value or null from `yamlMap`, otherwise throw.
|
||||
bool _tryReadBool(YamlMap yamlMap, String key, Logger logger) {
|
||||
final Object value = yamlMap[key];
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (value is! bool) {
|
||||
logger.printError('Expected "$key" to have a bool value, instead was "$value"');
|
||||
throw Exception();
|
||||
}
|
||||
return value as bool;
|
||||
}
|
||||
|
||||
// Try to read a `String` value or null from `yamlMap`, otherwise throw.
|
||||
String _tryReadString(YamlMap yamlMap, String key, Logger logger) {
|
||||
final Object value = yamlMap[key];
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (value is! String) {
|
||||
logger.printError('Expected "$key" to have a String value, instead was "$value"');
|
||||
throw Exception();
|
||||
}
|
||||
return value as String;
|
||||
}
|
||||
|
||||
// Try to read a valid `Uri` or null from `yamlMap`, otherwise throw.
|
||||
Uri _tryReadUri(YamlMap yamlMap, String key, Logger logger) {
|
||||
final String value = _tryReadString(yamlMap, key, logger);
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
final Uri uri = Uri.tryParse(value);
|
||||
if (uri == null) {
|
||||
logger.printError('"$value" must be a relative file URI');
|
||||
}
|
||||
return uri;
|
||||
}
|
|
@ -15,6 +15,7 @@ import '../build_system.dart';
|
|||
import '../depfile.dart';
|
||||
import 'assets.dart';
|
||||
import 'dart.dart';
|
||||
import 'localizations.dart';
|
||||
|
||||
/// Whether web builds should call the platform initialization logic.
|
||||
const String kInitializePlatform = 'InitializePlatform';
|
||||
|
@ -132,7 +133,8 @@ class Dart2JSTarget extends Target {
|
|||
|
||||
@override
|
||||
List<Target> get dependencies => const <Target>[
|
||||
WebEntrypointTarget()
|
||||
WebEntrypointTarget(),
|
||||
GenerateLocalizationsTarget(),
|
||||
];
|
||||
|
||||
@override
|
||||
|
|
|
@ -4,10 +4,10 @@
|
|||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:vm_service/vm_service.dart' as vm_service;
|
||||
import 'package:devtools_server/devtools_server.dart' as devtools_server;
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:package_config/package_config.dart';
|
||||
import 'package:vm_service/vm_service.dart' as vm_service;
|
||||
|
||||
import 'application_package.dart';
|
||||
import 'artifacts.dart';
|
||||
|
@ -21,7 +21,10 @@ import 'base/signals.dart';
|
|||
import 'base/terminal.dart';
|
||||
import 'base/utils.dart';
|
||||
import 'build_info.dart';
|
||||
import 'build_system/build_system.dart';
|
||||
import 'build_system/targets/localizations.dart';
|
||||
import 'bundle.dart';
|
||||
import 'cache.dart';
|
||||
import 'codegen.dart';
|
||||
import 'compile.dart';
|
||||
import 'convert.dart';
|
||||
|
@ -800,6 +803,40 @@ abstract class ResidentRunner {
|
|||
throw '${fullRestart ? 'Restart' : 'Reload'} is not supported in $mode mode';
|
||||
}
|
||||
|
||||
|
||||
BuildResult _lastBuild;
|
||||
Environment _environment;
|
||||
Future<void> runSourceGenerators() async {
|
||||
_environment ??= Environment(
|
||||
artifacts: globals.artifacts,
|
||||
logger: globals.logger,
|
||||
cacheDir: globals.cache.getRoot(),
|
||||
engineVersion: globals.flutterVersion.engineRevision,
|
||||
fileSystem: globals.fs,
|
||||
flutterRootDir: globals.fs.directory(Cache.flutterRoot),
|
||||
outputDir: globals.fs.directory(getBuildDirectory()),
|
||||
processManager: globals.processManager,
|
||||
projectDir: globals.fs.currentDirectory,
|
||||
);
|
||||
globals.logger.printTrace('Starting incremental build...');
|
||||
_lastBuild = await globals.buildSystem.buildIncremental(
|
||||
const GenerateLocalizationsTarget(),
|
||||
_environment,
|
||||
_lastBuild,
|
||||
);
|
||||
if (!_lastBuild.success) {
|
||||
for (final ExceptionMeasurement exceptionMeasurement in _lastBuild.exceptions.values) {
|
||||
globals.logger.printError(
|
||||
exceptionMeasurement.exception.toString(),
|
||||
stackTrace: globals.logger.isVerbose
|
||||
? exceptionMeasurement.stackTrace
|
||||
: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
globals.logger.printTrace('complete');
|
||||
}
|
||||
|
||||
/// Toggle whether canvaskit is being used for rendering, returning the new
|
||||
/// state.
|
||||
///
|
||||
|
@ -1200,6 +1237,7 @@ abstract class ResidentRunner {
|
|||
commandHelp.p.print();
|
||||
commandHelp.o.print();
|
||||
commandHelp.z.print();
|
||||
commandHelp.g.print();
|
||||
} else {
|
||||
commandHelp.S.print();
|
||||
commandHelp.U.print();
|
||||
|
@ -1337,6 +1375,9 @@ class TerminalHandler {
|
|||
case 'D':
|
||||
await residentRunner.detach();
|
||||
return true;
|
||||
case 'g':
|
||||
await residentRunner.runSourceGenerators();
|
||||
return true;
|
||||
case 'h':
|
||||
case 'H':
|
||||
case '?':
|
||||
|
|
|
@ -7,8 +7,8 @@ import 'package:package_config/package_config.dart';
|
|||
import 'package:vm_service/vm_service.dart' as vm_service;
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:pool/pool.dart';
|
||||
import 'base/async_guard.dart';
|
||||
|
||||
import 'base/async_guard.dart';
|
||||
import 'base/context.dart';
|
||||
import 'base/file_system.dart';
|
||||
import 'base/logger.dart';
|
||||
|
@ -365,6 +365,7 @@ class HotRunner extends ResidentRunner {
|
|||
// build, reducing overall initialization time. This is safe because the first
|
||||
// invocation of the frontend server produces a full dill file that the
|
||||
// subsequent invocation in devfs will not overwrite.
|
||||
await runSourceGenerators();
|
||||
if (device.generator != null) {
|
||||
startupTasks.add(
|
||||
device.generator.recompile(
|
||||
|
@ -674,6 +675,10 @@ class HotRunner extends ResidentRunner {
|
|||
emulator = false;
|
||||
}
|
||||
final Stopwatch timer = Stopwatch()..start();
|
||||
|
||||
// Run source generation if needed.
|
||||
await runSourceGenerators();
|
||||
|
||||
if (fullRestart) {
|
||||
final OperationResult result = await _fullRestartHelper(
|
||||
targetPlatform: targetPlatform,
|
||||
|
@ -1190,7 +1195,7 @@ class ProjectFileInvalidator {
|
|||
static const String _pubCachePathWindows = 'Pub/Cache';
|
||||
|
||||
// As of writing, Dart supports up to 32 asynchronous I/O threads per
|
||||
// isolate. We also want to avoid hitting platform limits on open file
|
||||
// isolate. We also want to avoid hitting platform limits on open file
|
||||
// handles/descriptors.
|
||||
//
|
||||
// This value was chosen based on empirical tests scanning a set of
|
||||
|
@ -1223,7 +1228,6 @@ class ProjectFileInvalidator {
|
|||
if (_isNotInPubCache(uri)) uri,
|
||||
];
|
||||
final List<Uri> invalidatedFiles = <Uri>[];
|
||||
|
||||
if (asyncScanning) {
|
||||
final Pool pool = Pool(_kMaxPendingStats);
|
||||
final List<Future<void>> waitList = <Future<void>>[];
|
||||
|
|
|
@ -57,6 +57,7 @@ void _testMessageLength({
|
|||
expect(commandHelp.U.toString().length, lessThanOrEqualTo(expectedWidth));
|
||||
expect(commandHelp.a.toString().length, lessThanOrEqualTo(expectedWidth));
|
||||
expect(commandHelp.d.toString().length, lessThanOrEqualTo(expectedWidth));
|
||||
expect(commandHelp.g.toString().length, lessThanOrEqualTo(expectedWidth));
|
||||
expect(commandHelp.h.toString().length, lessThanOrEqualTo(expectedWidth));
|
||||
expect(commandHelp.i.toString().length, lessThanOrEqualTo(expectedWidth));
|
||||
expect(commandHelp.k.toString().length, lessThanOrEqualTo(expectedWidth));
|
||||
|
@ -88,6 +89,7 @@ void main() {
|
|||
expect(commandHelp.U.toString(), startsWith('\x1B[1mU\x1B[22m'));
|
||||
expect(commandHelp.a.toString(), startsWith('\x1B[1ma\x1B[22m'));
|
||||
expect(commandHelp.d.toString(), startsWith('\x1B[1md\x1B[22m'));
|
||||
expect(commandHelp.g.toString(), startsWith('\x1B[1mg\x1B[22m'));
|
||||
expect(commandHelp.h.toString(), startsWith('\x1B[1mh\x1B[22m'));
|
||||
expect(commandHelp.i.toString(), startsWith('\x1B[1mi\x1B[22m'));
|
||||
expect(commandHelp.o.toString(), startsWith('\x1B[1mo\x1B[22m'));
|
||||
|
@ -164,6 +166,7 @@ void main() {
|
|||
expect(commandHelp.U.toString(), equals('\x1B[1mU\x1B[22m Dump accessibility tree in inverse hit test order. \x1B[1;30m(debugDumpSemantics)\x1B[39m'));
|
||||
expect(commandHelp.a.toString(), equals('\x1B[1ma\x1B[22m Toggle timeline events for all widget build methods. \x1B[1;30m(debugProfileWidgetBuilds)\x1B[39m'));
|
||||
expect(commandHelp.d.toString(), equals('\x1B[1md\x1B[22m Detach (terminate "flutter run" but leave application running).'));
|
||||
expect(commandHelp.g.toString(), equals('\x1B[1mg\x1B[22m Run source code generators.'));
|
||||
expect(commandHelp.h.toString(), equals('\x1B[1mh\x1B[22m Repeat this help message.'));
|
||||
expect(commandHelp.i.toString(), equals('\x1B[1mi\x1B[22m Toggle widget inspector. \x1B[1;30m(WidgetsApp.showWidgetInspectorOverride)\x1B[39m'));
|
||||
expect(commandHelp.o.toString(), equals('\x1B[1mo\x1B[22m Simulate different operating systems. \x1B[1;30m(defaultTargetPlatform)\x1B[39m'));
|
||||
|
@ -190,6 +193,7 @@ void main() {
|
|||
expect(commandHelp.U.toString(), equals('U Dump accessibility tree in inverse hit test order. (debugDumpSemantics)'));
|
||||
expect(commandHelp.a.toString(), equals('a Toggle timeline events for all widget build methods. (debugProfileWidgetBuilds)'));
|
||||
expect(commandHelp.d.toString(), equals('d Detach (terminate "flutter run" but leave application running).'));
|
||||
expect(commandHelp.g.toString(), equals('g Run source code generators.'));
|
||||
expect(commandHelp.h.toString(), equals('h Repeat this help message.'));
|
||||
expect(commandHelp.i.toString(), equals('i Toggle widget inspector. (WidgetsApp.showWidgetInspectorOverride)'));
|
||||
expect(commandHelp.o.toString(), equals('o Simulate different operating systems. (defaultTargetPlatform)'));
|
||||
|
|
|
@ -581,6 +581,50 @@ void main() {
|
|||
expect(fileSystem.file('output/debug'), isNot(exists));
|
||||
expect(fileSystem.file('output/release'), exists);
|
||||
});
|
||||
|
||||
testWithoutContext('A target using canSkip can create a conditional output', () async {
|
||||
final BuildSystem buildSystem = setUpBuildSystem(fileSystem);
|
||||
final File bar = environment.buildDir.childFile('bar');
|
||||
final File foo = environment.buildDir.childFile('foo');
|
||||
|
||||
// The target will write a file `foo`, but only if `bar` already exists.
|
||||
final TestTarget target = TestTarget(
|
||||
(Environment environment) async {
|
||||
foo.writeAsStringSync(bar.readAsStringSync());
|
||||
environment.buildDir
|
||||
.childFile('example.d')
|
||||
.writeAsStringSync('${foo.path}: ${bar.path}');
|
||||
},
|
||||
(Environment environment) {
|
||||
return !environment.buildDir.childFile('bar').existsSync();
|
||||
}
|
||||
)
|
||||
..depfiles = const <String>['example.d'];
|
||||
|
||||
// bar does not exist, there should be no inputs/outputs.
|
||||
final BuildResult firstResult = await buildSystem.build(target, environment);
|
||||
|
||||
expect(foo, isNot(exists));
|
||||
expect(firstResult.inputFiles, isEmpty);
|
||||
expect(firstResult.outputFiles, isEmpty);
|
||||
|
||||
// bar is created, the target should be able to run.
|
||||
bar.writeAsStringSync('content-1');
|
||||
final BuildResult secondResult = await buildSystem.build(target, environment);
|
||||
|
||||
expect(foo, exists);
|
||||
expect(secondResult.inputFiles.map((File file) => file.path), <String>[bar.path]);
|
||||
expect(secondResult.outputFiles.map((File file) => file.path), <String>[foo.path]);
|
||||
|
||||
// bar is destroyed, foo is also deleted.
|
||||
bar.deleteSync();
|
||||
final BuildResult thirdResult = await buildSystem.build(target, environment);
|
||||
|
||||
expect(foo, isNot(exists));
|
||||
expect(thirdResult.inputFiles, isEmpty);
|
||||
expect(thirdResult.outputFiles, isEmpty);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
BuildSystem setUpBuildSystem(FileSystem fileSystem) {
|
||||
|
@ -592,10 +636,20 @@ BuildSystem setUpBuildSystem(FileSystem fileSystem) {
|
|||
}
|
||||
|
||||
class TestTarget extends Target {
|
||||
TestTarget([this._build]);
|
||||
TestTarget([this._build, this._canSkip]);
|
||||
|
||||
final Future<void> Function(Environment environment) _build;
|
||||
|
||||
final bool Function(Environment environment) _canSkip;
|
||||
|
||||
@override
|
||||
bool canSkip(Environment environment) {
|
||||
if (_canSkip != null) {
|
||||
return _canSkip(environment);
|
||||
}
|
||||
return super.canSkip(environment);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> build(Environment environment) => _build(environment);
|
||||
|
||||
|
|
|
@ -0,0 +1,131 @@
|
|||
// 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/memory.dart';
|
||||
import 'package:flutter_tools/src/base/file_system.dart';
|
||||
import 'package:flutter_tools/src/base/logger.dart';
|
||||
import 'package:flutter_tools/src/build_system/build_system.dart';
|
||||
import 'package:flutter_tools/src/build_system/targets/localizations.dart';
|
||||
|
||||
import '../../../src/common.dart';
|
||||
import '../../../src/context.dart';
|
||||
|
||||
void main() {
|
||||
// Verifies that values are correctly passed through the localizations
|
||||
// target, but does not validate them beyond the serialized data type.
|
||||
testWithoutContext('generateLocalizations forwards arguments correctly', () async {
|
||||
final FileSystem fileSystem = MemoryFileSystem.test();
|
||||
final Logger logger = BufferLogger.test();
|
||||
final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
|
||||
const FakeCommand(
|
||||
command: <String>[
|
||||
'dart',
|
||||
'dev/tools/localization/bin/gen_l10n.dart',
|
||||
'--gen-inputs-and-outputs-list=/',
|
||||
'--arb-dir=arb',
|
||||
'--template-arb-file=example.arb',
|
||||
'--output-localization-file=bar',
|
||||
'--untranslated-messages-file=untranslated',
|
||||
'--output-class=Foo',
|
||||
'--header-file=header',
|
||||
'--header=HEADER',
|
||||
'--use-deferred-loading',
|
||||
'--preferred-supported-locales=en_US'
|
||||
],
|
||||
),
|
||||
]);
|
||||
final Directory arbDirectory = fileSystem.directory('arb')
|
||||
..createSync();
|
||||
arbDirectory.childFile('foo.arb').createSync();
|
||||
arbDirectory.childFile('bar.arb').createSync();
|
||||
|
||||
final LocalizationOptions options = LocalizationOptions(
|
||||
header: 'HEADER',
|
||||
headerFile: Uri.file('header'),
|
||||
arbDirectory: Uri.file('arb'),
|
||||
deferredLoading: true,
|
||||
outputClass: 'Foo',
|
||||
outputLocalizationsFile: Uri.file('bar'),
|
||||
preferredSupportedLocales: 'en_US',
|
||||
templateArbFile: Uri.file('example.arb'),
|
||||
untranslatedMessagesFile: Uri.file('untranslated'),
|
||||
);
|
||||
await generateLocalizations(
|
||||
options: options,
|
||||
logger: logger,
|
||||
fileSystem: fileSystem,
|
||||
processManager: processManager,
|
||||
projectDir: fileSystem.currentDirectory,
|
||||
dartBinaryPath: 'dart',
|
||||
flutterRoot: '',
|
||||
dependenciesDir: fileSystem.currentDirectory,
|
||||
);
|
||||
|
||||
expect(processManager.hasRemainingExpectations, false);
|
||||
});
|
||||
|
||||
testWithoutContext('generateLocalizations is skipped if l10n.yaml does not exist.', () async {
|
||||
final FileSystem fileSystem = MemoryFileSystem.test();
|
||||
final Environment environment = Environment.test(
|
||||
fileSystem.currentDirectory,
|
||||
artifacts: null,
|
||||
fileSystem: fileSystem,
|
||||
logger: BufferLogger.test(),
|
||||
processManager: FakeProcessManager.any(),
|
||||
);
|
||||
|
||||
expect(const GenerateLocalizationsTarget().canSkip(environment), true);
|
||||
|
||||
environment.projectDir.childFile('l10n.yaml').createSync();
|
||||
|
||||
expect(const GenerateLocalizationsTarget().canSkip(environment), false);
|
||||
});
|
||||
|
||||
testWithoutContext('parseLocalizationsOptions handles valid yaml configuration', () async {
|
||||
final FileSystem fileSystem = MemoryFileSystem.test();
|
||||
final File configFile = fileSystem.file('l10n.yaml')
|
||||
..writeAsStringSync('''
|
||||
arb-dir: arb
|
||||
template-arb-file: example.arb
|
||||
output-localization-file: bar
|
||||
untranslated-messages-file: untranslated
|
||||
output-class: Foo
|
||||
header-file: header
|
||||
header: HEADER
|
||||
use-deferred-loading: true
|
||||
preferred-supported-locales: en_US
|
||||
''');
|
||||
|
||||
final LocalizationOptions options = parseLocalizationsOptions(
|
||||
file: configFile,
|
||||
logger: BufferLogger.test(),
|
||||
);
|
||||
|
||||
expect(options.arbDirectory, Uri.parse('arb'));
|
||||
expect(options.templateArbFile, Uri.parse('example.arb'));
|
||||
expect(options.outputLocalizationsFile, Uri.parse('bar'));
|
||||
expect(options.untranslatedMessagesFile, Uri.parse('untranslated'));
|
||||
expect(options.outputClass, 'Foo');
|
||||
expect(options.headerFile, Uri.parse('header'));
|
||||
expect(options.header, 'HEADER');
|
||||
expect(options.deferredLoading, true);
|
||||
expect(options.preferredSupportedLocales, 'en_US');
|
||||
});
|
||||
|
||||
testWithoutContext('parseLocalizationsOptions throws exception on invalid yaml configuration', () async {
|
||||
final FileSystem fileSystem = MemoryFileSystem.test();
|
||||
final File configFile = fileSystem.file('l10n.yaml')
|
||||
..writeAsStringSync('''
|
||||
use-deferred-loading: string
|
||||
''');
|
||||
|
||||
expect(
|
||||
() => parseLocalizationsOptions(
|
||||
file: configFile,
|
||||
logger: BufferLogger.test(),
|
||||
),
|
||||
throwsA(isA<Exception>()),
|
||||
);
|
||||
});
|
||||
}
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_tools/src/cache.dart';
|
||||
import 'package:vm_service/vm_service.dart' as vm_service;
|
||||
import 'package:file/memory.dart';
|
||||
import 'package:file_testing/file_testing.dart';
|
||||
|
@ -537,6 +538,61 @@ void main() {
|
|||
expect(cacheDill, isNot(exists));
|
||||
}));
|
||||
|
||||
test('ResidentRunner can run source generation', () => testbed.run(() async {
|
||||
final FakeProcessManager processManager = globals.processManager as FakeProcessManager;
|
||||
final Directory dependencies = globals.fs.directory(
|
||||
globals.fs.path.join('build', '6ec2559087977927717927ede0a147f1'));
|
||||
processManager.addCommand(FakeCommand(
|
||||
command: <String>[
|
||||
globals.artifacts.getArtifactPath(Artifact.engineDartBinary),
|
||||
globals.fs.path.join(Cache.flutterRoot, 'dev', 'tools', 'localization', 'bin', 'gen_l10n.dart'),
|
||||
'--gen-inputs-and-outputs-list=${dependencies.absolute.path}',
|
||||
],
|
||||
onRun: () {
|
||||
dependencies
|
||||
.childFile('gen_l10n_inputs_and_outputs.json')
|
||||
..createSync()
|
||||
..writeAsStringSync('{"inputs":[],"outputs":[]}');
|
||||
}
|
||||
));
|
||||
globals.fs.file(globals.fs.path.join('lib', 'l10n', 'foo.arb'))
|
||||
.createSync(recursive: true);
|
||||
globals.fs.file('l10n.yaml').createSync();
|
||||
|
||||
await residentRunner.runSourceGenerators();
|
||||
|
||||
expect(testLogger.errorText, isEmpty);
|
||||
}, overrides: <Type, Generator>{
|
||||
ProcessManager: () => FakeProcessManager.list(<FakeCommand>[]),
|
||||
}));
|
||||
|
||||
test('ResidentRunner can run source generation - generation fails', () => testbed.run(() async {
|
||||
final FakeProcessManager processManager = globals.processManager as FakeProcessManager;
|
||||
final Directory dependencies = globals.fs.directory(
|
||||
globals.fs.path.join('build', '6ec2559087977927717927ede0a147f1'));
|
||||
processManager.addCommand(FakeCommand(
|
||||
command: <String>[
|
||||
globals.artifacts.getArtifactPath(Artifact.engineDartBinary),
|
||||
globals.fs.path.join(Cache.flutterRoot, 'dev', 'tools', 'localization', 'bin', 'gen_l10n.dart'),
|
||||
'--gen-inputs-and-outputs-list=${dependencies.absolute.path}',
|
||||
],
|
||||
exitCode: 1,
|
||||
stderr: 'stderr'
|
||||
));
|
||||
globals.fs.file(globals.fs.path.join('lib', 'l10n', 'foo.arb'))
|
||||
.createSync(recursive: true);
|
||||
globals.fs.file('l10n.yaml').createSync();
|
||||
|
||||
await residentRunner.runSourceGenerators();
|
||||
|
||||
expect(testLogger.errorText, allOf(
|
||||
contains('stderr'), // Message from gen_l10n.dart
|
||||
contains('Exception') // Message from build_system
|
||||
));
|
||||
}, overrides: <Type, Generator>{
|
||||
ProcessManager: () => FakeProcessManager.list(<FakeCommand>[]),
|
||||
}));
|
||||
|
||||
test('ResidentRunner printHelpDetails', () => testbed.run(() {
|
||||
fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[]);
|
||||
when(mockDevice.supportsHotRestart).thenReturn(true);
|
||||
|
@ -573,6 +629,7 @@ void main() {
|
|||
commandHelp.p,
|
||||
commandHelp.o,
|
||||
commandHelp.z,
|
||||
commandHelp.g,
|
||||
commandHelp.M,
|
||||
commandHelp.v,
|
||||
commandHelp.P,
|
||||
|
|
|
@ -6,15 +6,14 @@ import 'dart:async';
|
|||
|
||||
import 'package:file/file.dart';
|
||||
import 'package:flutter_tools/src/base/file_system.dart';
|
||||
import 'package:flutter_tools/src/base/io.dart';
|
||||
import 'package:flutter_tools/src/globals.dart' as globals;
|
||||
import 'package:process/process.dart';
|
||||
|
||||
import '../src/common.dart';
|
||||
import 'test_data/gen_l10n_project.dart';
|
||||
import 'test_driver.dart';
|
||||
import 'test_utils.dart';
|
||||
|
||||
final GenL10nProject project = GenL10nProject();
|
||||
|
||||
// Verify that the code generated by gen_l10n executes correctly.
|
||||
// It can fail if gen_l10n produces a lib/l10n/app_localizations.dart that:
|
||||
// - Does not analyze cleanly.
|
||||
|
@ -23,50 +22,23 @@ import 'test_utils.dart';
|
|||
// loaded workstation, so the test could time out on a heavily loaded bot.
|
||||
void main() {
|
||||
Directory tempDir;
|
||||
final GenL10nProject _project = GenL10nProject();
|
||||
FlutterRunTestDriver _flutter;
|
||||
FlutterRunTestDriver flutter;
|
||||
|
||||
setUp(() async {
|
||||
tempDir = createResolvedTempDirectorySync('gen_l10n_test.');
|
||||
await _project.setUpIn(tempDir);
|
||||
_flutter = FlutterRunTestDriver(tempDir);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await _flutter.stop();
|
||||
await flutter.stop();
|
||||
tryToDelete(tempDir);
|
||||
});
|
||||
|
||||
void runCommand(List<String> command) {
|
||||
final ProcessResult result = const LocalProcessManager().runSync(
|
||||
command,
|
||||
workingDirectory: tempDir.path,
|
||||
environment: <String, String>{ 'FLUTTER_ROOT': getFlutterRoot() },
|
||||
);
|
||||
if (result.exitCode != 0) {
|
||||
throw Exception('FAILED [${result.exitCode}]: ${command.join(' ')}\n${result.stderr}\n${result.stdout}');
|
||||
}
|
||||
}
|
||||
|
||||
void setUpAndRunGenL10n({List<String> args}) {
|
||||
// Get the intl packages before running gen_l10n.
|
||||
final String flutterBin = globals.platform.isWindows ? 'flutter.bat' : 'flutter';
|
||||
final String flutterPath = globals.fs.path.join(getFlutterRoot(), 'bin', flutterBin);
|
||||
runCommand(<String>[flutterPath, 'pub', 'get']);
|
||||
|
||||
// Generate lib/l10n/app_localizations.dart
|
||||
final String genL10nPath = globals.fs.path.join(getFlutterRoot(), 'dev', 'tools', 'localization', 'bin', 'gen_l10n.dart');
|
||||
final String dartBin = globals.platform.isWindows ? 'dart.exe' : 'dart';
|
||||
final String dartPath = globals.fs.path.join(getFlutterRoot(), 'bin', 'cache', 'dart-sdk', 'bin', dartBin);
|
||||
runCommand(<String>[dartPath, genL10nPath, args?.join(' ')]);
|
||||
}
|
||||
|
||||
Future<StringBuffer> runApp() async {
|
||||
// Run the app defined in GenL10nProject.main and wait for it to
|
||||
// send '#l10n END' to its stdout.
|
||||
final Completer<void> l10nEnd = Completer<void>();
|
||||
final StringBuffer stdout = StringBuffer();
|
||||
final StreamSubscription<String> subscription = _flutter.stdout.listen((String line) {
|
||||
final StreamSubscription<String> subscription = flutter.stdout.listen((String line) {
|
||||
if (line.contains('#l10n')) {
|
||||
stdout.writeln(line.substring(line.indexOf('#l10n')));
|
||||
}
|
||||
|
@ -74,7 +46,7 @@ void main() {
|
|||
l10nEnd.complete();
|
||||
}
|
||||
});
|
||||
await _flutter.run();
|
||||
await flutter.run();
|
||||
await l10nEnd.future;
|
||||
await subscription.cancel();
|
||||
return stdout;
|
||||
|
@ -180,13 +152,15 @@ void main() {
|
|||
}
|
||||
|
||||
test('generated l10n classes produce expected localized strings', () async {
|
||||
setUpAndRunGenL10n();
|
||||
await project.setUpIn(tempDir);
|
||||
flutter = FlutterRunTestDriver(tempDir);
|
||||
final StringBuffer stdout = await runApp();
|
||||
expectOutput(stdout);
|
||||
});
|
||||
|
||||
test('generated l10n classes produce expected localized strings with deferred loading', () async {
|
||||
setUpAndRunGenL10n(args: <String>['--use-deferred-loading']);
|
||||
await project.setUpIn(tempDir, useDeferredLoading: true);
|
||||
flutter = FlutterRunTestDriver(tempDir);
|
||||
final StringBuffer stdout = await runApp();
|
||||
expectOutput(stdout);
|
||||
});
|
||||
|
|
|
@ -7,13 +7,16 @@ import 'dart:async';
|
|||
import 'package:file/file.dart';
|
||||
import 'package:flutter_tools/src/base/file_system.dart';
|
||||
import 'package:flutter_tools/src/globals.dart' as globals;
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
import '../test_utils.dart';
|
||||
import 'project.dart';
|
||||
|
||||
class GenL10nProject extends Project {
|
||||
@override
|
||||
Future<void> setUpIn(Directory dir) {
|
||||
Future<void> setUpIn(Directory dir, {
|
||||
bool useDeferredLoading = false,
|
||||
}) {
|
||||
this.dir = dir;
|
||||
writeFile(globals.fs.path.join(dir.path, 'lib', 'l10n', 'app_en.arb'), appEn);
|
||||
writeFile(globals.fs.path.join(dir.path, 'lib', 'l10n', 'app_en_CA.arb'), appEnCa);
|
||||
|
@ -24,6 +27,7 @@ class GenL10nProject extends Project {
|
|||
writeFile(globals.fs.path.join(dir.path, 'lib', 'l10n', 'app_zh_Hant.arb'), appZhHant);
|
||||
writeFile(globals.fs.path.join(dir.path, 'lib', 'l10n', 'app_zh_Hans.arb'), appZhHans);
|
||||
writeFile(globals.fs.path.join(dir.path, 'lib', 'l10n', 'app_zh_Hant_TW.arb'), appZhHantTw);
|
||||
writeFile(globals.fs.path.join(dir.path, 'l10n.yaml'), l10nYaml(useDeferredLoading: useDeferredLoading));
|
||||
return super.setUpIn(dir);
|
||||
}
|
||||
|
||||
|
@ -561,4 +565,15 @@ void main() {
|
|||
"helloWorld": "台灣繁體你好世界"
|
||||
}
|
||||
''';
|
||||
|
||||
String l10nYaml({
|
||||
@required bool useDeferredLoading,
|
||||
}) {
|
||||
if (useDeferredLoading) {
|
||||
return r'''
|
||||
use-deferred-loading: false
|
||||
''';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue