diff --git a/packages/flutter_tools/lib/src/base/command_help.dart b/packages/flutter_tools/lib/src/base/command_help.dart index 8886659af2..9d1f56d796 100644 --- a/packages/flutter_tools/lib/src/base/command_help.dart +++ b/packages/flutter_tools/lib/src/base/command_help.dart @@ -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', diff --git a/packages/flutter_tools/lib/src/build_runner/resident_web_runner.dart b/packages/flutter_tools/lib/src/build_runner/resident_web_runner.dart index a25a62a71d..033f29020d 100644 --- a/packages/flutter_tools/lib/src/build_runner/resident_web_runner.dart +++ b/packages/flutter_tools/lib/src/build_runner/resident_web_runner.dart @@ -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) { diff --git a/packages/flutter_tools/lib/src/build_system/build_system.dart b/packages/flutter_tools/lib/src/build_system/build_system.dart index c2d7d74942..8a23b344d8 100644 --- a/packages/flutter_tools/lib/src/build_system/build_system.dart +++ b/packages/flutter_tools/lib/src/build_system/build_system.dart @@ -134,6 +134,12 @@ abstract class Target { /// A list of zero or more depfiles, located directly under {BUILD_DIR}. List get depfiles => const []; + /// 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 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); diff --git a/packages/flutter_tools/lib/src/build_system/targets/dart.dart b/packages/flutter_tools/lib/src/build_system/targets/dart.dart index 94ac5c4fc0..a084fd7728 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/dart.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/dart.dart @@ -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 get dependencies => []; + List get dependencies => const [ + GenerateLocalizationsTarget(), + ]; @override Future build(Environment environment) async { diff --git a/packages/flutter_tools/lib/src/build_system/targets/localizations.dart b/packages/flutter_tools/lib/src/build_system/targets/localizations.dart new file mode 100644 index 0000000000..95539e9204 --- /dev/null +++ b/packages/flutter_tools/lib/src/build_system/targets/localizations.dart @@ -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 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([ + 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 get dependencies => []; + + @override + List get inputs => [ + // 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 get outputs => []; + + @override + List get depfiles => ['gen_localizations.d']; + + @override + bool canSkip(Environment environment) { + final File configFile = environment.projectDir.childFile('l10n.yaml'); + return !configFile.existsSync(); + } + + @override + Future 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 dependencies = json + .decode(environment.buildDir.childFile(_kDependenciesFileName).readAsStringSync()) as Map; + final Depfile depfile = Depfile( + [ + configFile, + for (dynamic inputFile in dependencies['inputs'] as List) + environment.fileSystem.file(inputFile) + ], + [ + for (dynamic outputFile in dependencies['outputs'] as List) + 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; +} diff --git a/packages/flutter_tools/lib/src/build_system/targets/web.dart b/packages/flutter_tools/lib/src/build_system/targets/web.dart index be58660935..b16a7cd79b 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/web.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/web.dart @@ -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 get dependencies => const [ - WebEntrypointTarget() + WebEntrypointTarget(), + GenerateLocalizationsTarget(), ]; @override diff --git a/packages/flutter_tools/lib/src/resident_runner.dart b/packages/flutter_tools/lib/src/resident_runner.dart index 647cdbe1da..30dbedcdd9 100644 --- a/packages/flutter_tools/lib/src/resident_runner.dart +++ b/packages/flutter_tools/lib/src/resident_runner.dart @@ -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 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 '?': diff --git a/packages/flutter_tools/lib/src/run_hot.dart b/packages/flutter_tools/lib/src/run_hot.dart index 3526131e5d..33bd40e30b 100644 --- a/packages/flutter_tools/lib/src/run_hot.dart +++ b/packages/flutter_tools/lib/src/run_hot.dart @@ -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 invalidatedFiles = []; - if (asyncScanning) { final Pool pool = Pool(_kMaxPendingStats); final List> waitList = >[]; diff --git a/packages/flutter_tools/test/general.shard/base/command_help_test.dart b/packages/flutter_tools/test/general.shard/base/command_help_test.dart index 07ad962b6e..40f7d01fca 100644 --- a/packages/flutter_tools/test/general.shard/base/command_help_test.dart +++ b/packages/flutter_tools/test/general.shard/base/command_help_test.dart @@ -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)')); diff --git a/packages/flutter_tools/test/general.shard/build_system/build_system_test.dart b/packages/flutter_tools/test/general.shard/build_system/build_system_test.dart index 05ce1d47cd..208ed1f751 100644 --- a/packages/flutter_tools/test/general.shard/build_system/build_system_test.dart +++ b/packages/flutter_tools/test/general.shard/build_system/build_system_test.dart @@ -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 ['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), [bar.path]); + expect(secondResult.outputFiles.map((File file) => file.path), [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 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 build(Environment environment) => _build(environment); diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/localizations_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/localizations_test.dart new file mode 100644 index 0000000000..ce781c3f9a --- /dev/null +++ b/packages/flutter_tools/test/general.shard/build_system/targets/localizations_test.dart @@ -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([ + const FakeCommand( + command: [ + '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()), + ); + }); +} diff --git a/packages/flutter_tools/test/general.shard/resident_runner_test.dart b/packages/flutter_tools/test/general.shard/resident_runner_test.dart index 6757be6f7a..2635ba66fb 100644 --- a/packages/flutter_tools/test/general.shard/resident_runner_test.dart +++ b/packages/flutter_tools/test/general.shard/resident_runner_test.dart @@ -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: [ + 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: { + ProcessManager: () => FakeProcessManager.list([]), + })); + + 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: [ + 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: { + ProcessManager: () => FakeProcessManager.list([]), + })); + test('ResidentRunner printHelpDetails', () => testbed.run(() { fakeVmServiceHost = FakeVmServiceHost(requests: []); 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, diff --git a/packages/flutter_tools/test/integration.shard/gen_l10n_test.dart b/packages/flutter_tools/test/integration.shard/gen_l10n_test.dart index deda717fc3..4696201d59 100644 --- a/packages/flutter_tools/test/integration.shard/gen_l10n_test.dart +++ b/packages/flutter_tools/test/integration.shard/gen_l10n_test.dart @@ -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 command) { - final ProcessResult result = const LocalProcessManager().runSync( - command, - workingDirectory: tempDir.path, - environment: { 'FLUTTER_ROOT': getFlutterRoot() }, - ); - if (result.exitCode != 0) { - throw Exception('FAILED [${result.exitCode}]: ${command.join(' ')}\n${result.stderr}\n${result.stdout}'); - } - } - - void setUpAndRunGenL10n({List 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([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([dartPath, genL10nPath, args?.join(' ')]); - } - Future runApp() async { // Run the app defined in GenL10nProject.main and wait for it to // send '#l10n END' to its stdout. final Completer l10nEnd = Completer(); final StringBuffer stdout = StringBuffer(); - final StreamSubscription subscription = _flutter.stdout.listen((String line) { + final StreamSubscription 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: ['--use-deferred-loading']); + await project.setUpIn(tempDir, useDeferredLoading: true); + flutter = FlutterRunTestDriver(tempDir); final StringBuffer stdout = await runApp(); expectOutput(stdout); }); diff --git a/packages/flutter_tools/test/integration.shard/test_data/gen_l10n_project.dart b/packages/flutter_tools/test/integration.shard/test_data/gen_l10n_project.dart index e503c59685..233320566b 100644 --- a/packages/flutter_tools/test/integration.shard/test_data/gen_l10n_project.dart +++ b/packages/flutter_tools/test/integration.shard/test_data/gen_l10n_project.dart @@ -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 setUpIn(Directory dir) { + Future 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 ''; + } }