diff --git a/dev/benchmarks/test_apps/stocks/l10n.yaml b/dev/benchmarks/test_apps/stocks/l10n.yaml index baea72d8014..9900c77ed38 100644 --- a/dev/benchmarks/test_apps/stocks/l10n.yaml +++ b/dev/benchmarks/test_apps/stocks/l10n.yaml @@ -2,16 +2,17 @@ ## `arb-dir` sets the input directory. The output directory will match ## the input directory if the output directory is not set. arb-dir: lib/i18n -## `template-arb-file` describes the template arb file that the tool -## will use to check and validate the remaining arb files when -## generating Flutter's localization files. -template-arb-file: stocks_en.arb -## `output-localization-file` is the name of the generated file. -output-localization-file: stock_strings.dart +## `header-file` is the file that contains a custom +## header for each of the generated files. +header-file: header.txt ## `output-class` is the name of the localizations class your ## Flutter application will use. The file will need to be ## imported throughout your application. output-class: StockStrings -## `header-file` is the file that contains a custom -## header for each of the generated files. -header-file: header.txt +## `output-localization-file` is the name of the generated file. +output-localization-file: stock_strings.dart +## `template-arb-file` describes the template arb file that the tool +## will use to check and validate the remaining arb files when +## generating Flutter's localization files. +synthetic-package: false +template-arb-file: stocks_en.arb diff --git a/dev/tools/localization/bin/gen_l10n.dart b/dev/tools/localization/bin/gen_l10n.dart index 8b21cfabd00..d9da4ef410d 100644 --- a/dev/tools/localization/bin/gen_l10n.dart +++ b/dev/tools/localization/bin/gen_l10n.dart @@ -27,10 +27,15 @@ void main(List arguments) { ); parser.addOption( 'output-dir', - help: 'The directory where the generated localization classes will be written. ' + help: 'The directory where the generated localization classes will be written ' + 'if the synthetic-package flag is set to false.' + '\n\n' + 'If output-dir is specified and the synthetic-package flag is enabled, ' + 'this option will be ignored by the tool.' + '\n\n' 'The app must import the file specified in the \'output-localization-file\' ' 'option from this directory. If unspecified, this defaults to the same ' - 'directory as the input directory specified in \'arb-dir\'.' + 'directory as the input directory specified in \'arb-dir\'.', ); parser.addOption( 'template-arb-file', @@ -120,6 +125,20 @@ void main(List arguments) { '\n\n' 'When null, the JSON file will not be generated.' ); + parser.addFlag( + 'synthetic-package', + defaultsTo: true, + help: 'Determines whether or not the generated output files will be ' + 'generated as a synthetic package or at a specified directory in ' + 'the Flutter project.' + '\n\n' + 'This flag is set to true by default.' + '\n\n' + 'When synthetic-package is set to false, it will generate the ' + 'localizations files in the directory specified by arb-dir by default. ' + '\n\n' + 'If output-dir is specified, files will be generated there.', + ); parser.addOption( 'project-dir', valueHelp: 'absolute/path/to/flutter/project', @@ -148,6 +167,7 @@ void main(List arguments) { final String headerFile = results['header-file'] as String; final bool useDeferredLoading = results['use-deferred-loading'] as bool; final String inputsAndOutputsListPath = results['gen-inputs-and-outputs-list'] as String; + final bool useSyntheticPackage = results['synthetic-package'] as bool; final String projectPathString = results['project-dir'] as String; const local.LocalFileSystem fs = local.LocalFileSystem(); @@ -166,6 +186,7 @@ void main(List arguments) { headerFile: headerFile, useDeferredLoading: useDeferredLoading, inputsAndOutputsListPath: inputsAndOutputsListPath, + useSyntheticPackage: useSyntheticPackage, projectPathString: projectPathString, ) ..loadResources() diff --git a/dev/tools/localization/gen_l10n.dart b/dev/tools/localization/gen_l10n.dart index b4373e84119..95fd514ada1 100644 --- a/dev/tools/localization/gen_l10n.dart +++ b/dev/tools/localization/gen_l10n.dart @@ -13,6 +13,13 @@ import 'gen_l10n_templates.dart'; import 'gen_l10n_types.dart'; import 'localizations_utils.dart'; +/// The default path used when the `useSyntheticPackage` setting is set to true +/// in [LocalizationsGenerator]. +/// +/// See [LocalizationsGenerator.initialize] for where and how it is used by the +/// localizations tool. +final String defaultSyntheticPackagePath = path.join('.dart_tool', 'flutter_gen', 'gen_l10n'); + List generateMethodParameters(Message message) { assert(message.placeholders.isNotEmpty); final Placeholder countPlaceholder = message.isPlural ? message.getCountPlaceholder() : null; @@ -523,11 +530,15 @@ class LocalizationsGenerator { String headerFile, bool useDeferredLoading = false, String inputsAndOutputsListPath, + bool useSyntheticPackage = true, String projectPathString, }) { setProjectDir(projectPathString); setInputDirectory(inputPathString); - setOutputDirectory(outputPathString ?? inputPathString); + setOutputDirectory( + outputPathString: outputPathString ?? inputPathString, + useSyntheticPackage: useSyntheticPackage, + ); setTemplateArbFile(templateArbFileName); setBaseOutputFile(outputFileString); setPreferredSupportedLocales(preferredSupportedLocaleString); @@ -595,14 +606,29 @@ class LocalizationsGenerator { /// Sets the reference [Directory] for [outputDirectory]. @visibleForTesting - void setOutputDirectory(String outputPathString) { - if (outputPathString == null) - throw L10nException('outputPathString argument cannot be null'); - outputDirectory = _fs.directory( - projectDirectory != null - ? _getAbsoluteProjectPath(outputPathString) - : outputPathString - ); + void setOutputDirectory({ + String outputPathString, + bool useSyntheticPackage = true, + }) { + if (useSyntheticPackage) { + outputDirectory = _fs.directory( + projectDirectory != null + ? _getAbsoluteProjectPath(defaultSyntheticPackagePath) + : defaultSyntheticPackagePath + ); + } else { + if (outputPathString == null) + throw L10nException( + 'outputPathString argument cannot be null if not using ' + 'synthetic package option.' + ); + + outputDirectory = _fs.directory( + projectDirectory != null + ? _getAbsoluteProjectPath(outputPathString) + : outputPathString + ); + } } /// Sets the reference [File] for [templateArbFile]. @@ -716,6 +742,7 @@ class LocalizationsGenerator { _inputsAndOutputsListFile = _fs.file( path.join(inputsAndOutputsListPath, 'gen_l10n_inputs_and_outputs.json'), ); + _inputFileList = []; _outputFileList = []; } diff --git a/dev/tools/test/localization/gen_l10n_test.dart b/dev/tools/test/localization/gen_l10n_test.dart index 4e97537f727..b76995129da 100644 --- a/dev/tools/test/localization/gen_l10n_test.dart +++ b/dev/tools/test/localization/gen_l10n_test.dart @@ -16,6 +16,7 @@ import '../../localization/localizations_utils.dart'; import '../common.dart'; final String defaultL10nPathString = path.join('lib', 'l10n'); +final String syntheticPackagePath = path.join('.dart_tool', 'flutter_gen', 'gen_l10n'); const String defaultTemplateArbFileName = 'app_en.arb'; const String defaultOutputFileString = 'output-localization-file.dart'; const String defaultClassNameString = 'AppLocalizations'; @@ -98,21 +99,28 @@ void main() { ); }); - test('setOutputDirectory fails if output string is null', () { - _standardFlutterDirectoryL10nSetup(fs); - final LocalizationsGenerator generator = LocalizationsGenerator(fs); - try { - generator.setOutputDirectory(null); - } on L10nException catch (e) { - expect(e.message, contains('cannot be null')); - return; - } + test( + 'setOutputDirectory fails if output string is null while not using the ' + 'synthetic package option', + () { + _standardFlutterDirectoryL10nSetup(fs); + final LocalizationsGenerator generator = LocalizationsGenerator(fs); + try { + generator.setOutputDirectory( + outputPathString: null, + useSyntheticPackage: false, + ); + } on L10nException catch (e) { + expect(e.message, contains('cannot be null')); + return; + } - fail( - 'LocalizationsGenerator.setOutputDirectory should fail if the ' - 'input string is null.' - ); - }); + fail( + 'LocalizationsGenerator.setOutputDirectory should fail if the ' + 'input string is null.' + ); + }, + ); test('setTemplateArbFile fails if inputDirectory is null', () { final LocalizationsGenerator generator = LocalizationsGenerator(fs); @@ -232,8 +240,9 @@ void main() { expect( fs.isFileSync(path.join( flutterProjectPath, - 'lib', - 'l10n', + '.dart_tool', + 'flutter_gen', + 'gen_l10n', 'output-localization-file_en.dart', )), true, @@ -241,8 +250,9 @@ void main() { expect( fs.isFileSync(path.join( flutterProjectPath, - 'lib', - 'l10n', + '.dart_tool', + 'flutter_gen', + 'gen_l10n', 'output-localization-file_es.dart', )), true, @@ -289,7 +299,7 @@ void main() { generator = LocalizationsGenerator(fs); try { generator.setInputDirectory(defaultL10nPathString); - generator.setOutputDirectory(defaultL10nPathString); + generator.setOutputDirectory(); generator.setTemplateArbFile(defaultTemplateArbFileName); generator.setBaseOutputFile(defaultOutputFileString); } on L10nException catch (e) { @@ -420,7 +430,7 @@ void main() { fail('Generating output should not fail: \n${e.message}'); } - final Directory outputDirectory = fs.directory('lib').childDirectory('l10n'); + final Directory outputDirectory = fs.directory(syntheticPackagePath); expect(outputDirectory.childFile('output-localization-file.dart').existsSync(), isTrue); expect(outputDirectory.childFile('output-localization-file_en.dart').existsSync(), isTrue); expect(outputDirectory.childFile('output-localization-file_es.dart').existsSync(), isTrue); @@ -490,95 +500,111 @@ void main() { expect(unimplementedOutputString, contains('subtitle')); }); - test('uses inputPathString as outputPathString when the outputPathString is null', () { - _standardFlutterDirectoryL10nSetup(fs); - LocalizationsGenerator generator; - try { - generator = LocalizationsGenerator(fs); - generator - ..initialize( - inputPathString: defaultL10nPathString, - templateArbFileName: defaultTemplateArbFileName, - outputFileString: defaultOutputFileString, - classNameString: defaultClassNameString, - ) - ..loadResources() - ..writeOutputFiles(); - } on L10nException catch (e) { - fail('Generating output should not fail: \n${e.message}'); - } + test( + 'uses inputPathString as outputPathString when the outputPathString is ' + 'null while not using the synthetic package option', + () { + _standardFlutterDirectoryL10nSetup(fs); + LocalizationsGenerator generator; + try { + generator = LocalizationsGenerator(fs); + generator + ..initialize( + inputPathString: defaultL10nPathString, + // outputPathString is intentionally not defined + templateArbFileName: defaultTemplateArbFileName, + outputFileString: defaultOutputFileString, + classNameString: defaultClassNameString, + useSyntheticPackage: false, + ) + ..loadResources() + ..writeOutputFiles(); + } on L10nException catch (e) { + fail('Generating output should not fail: \n${e.message}'); + } - final Directory outputDirectory = fs.directory('lib').childDirectory('l10n'); - expect(outputDirectory.childFile('output-localization-file.dart').existsSync(), isTrue); - expect(outputDirectory.childFile('output-localization-file_en.dart').existsSync(), isTrue); - expect(outputDirectory.childFile('output-localization-file_es.dart').existsSync(), isTrue); - }); + final Directory outputDirectory = fs.directory('lib').childDirectory('l10n'); + expect(outputDirectory.childFile('output-localization-file.dart').existsSync(), isTrue); + expect(outputDirectory.childFile('output-localization-file_en.dart').existsSync(), isTrue); + expect(outputDirectory.childFile('output-localization-file_es.dart').existsSync(), isTrue); + }, + ); - test('correctly generates output files in non-default output directory if it already exists', () { - final Directory l10nDirectory = fs.currentDirectory - .childDirectory('lib') - .childDirectory('l10n') - ..createSync(recursive: true); - // Create the directory 'lib/l10n/output'. - l10nDirectory.childDirectory('output'); + test( + 'correctly generates output files in non-default output directory if it ' + 'already exists while not using the synthetic package option', + () { + final Directory l10nDirectory = fs.currentDirectory + .childDirectory('lib') + .childDirectory('l10n') + ..createSync(recursive: true); + // Create the directory 'lib/l10n/output'. + l10nDirectory.childDirectory('output'); - l10nDirectory - .childFile(defaultTemplateArbFileName) - .writeAsStringSync(singleMessageArbFileString); - l10nDirectory - .childFile(esArbFileName) - .writeAsStringSync(singleEsMessageArbFileString); + l10nDirectory + .childFile(defaultTemplateArbFileName) + .writeAsStringSync(singleMessageArbFileString); + l10nDirectory + .childFile(esArbFileName) + .writeAsStringSync(singleEsMessageArbFileString); - LocalizationsGenerator generator; - try { - generator = LocalizationsGenerator(fs); - generator - ..initialize( - inputPathString: defaultL10nPathString, - outputPathString: path.join('lib', 'l10n', 'output'), - templateArbFileName: defaultTemplateArbFileName, - outputFileString: defaultOutputFileString, - classNameString: defaultClassNameString, - ) - ..loadResources() - ..writeOutputFiles(); - } on L10nException catch (e) { - fail('Generating output should not fail: \n${e.message}'); - } + LocalizationsGenerator generator; + try { + generator = LocalizationsGenerator(fs); + generator + ..initialize( + inputPathString: defaultL10nPathString, + outputPathString: path.join('lib', 'l10n', 'output'), + templateArbFileName: defaultTemplateArbFileName, + outputFileString: defaultOutputFileString, + classNameString: defaultClassNameString, + useSyntheticPackage: false, + ) + ..loadResources() + ..writeOutputFiles(); + } on L10nException catch (e) { + fail('Generating output should not fail: \n${e.message}'); + } - final Directory outputDirectory = fs.directory('lib').childDirectory('l10n').childDirectory('output'); - expect(outputDirectory.existsSync(), isTrue); - expect(outputDirectory.childFile('output-localization-file.dart').existsSync(), isTrue); - expect(outputDirectory.childFile('output-localization-file_en.dart').existsSync(), isTrue); - expect(outputDirectory.childFile('output-localization-file_es.dart').existsSync(), isTrue); - }); + final Directory outputDirectory = fs.directory('lib').childDirectory('l10n').childDirectory('output'); + expect(outputDirectory.existsSync(), isTrue); + expect(outputDirectory.childFile('output-localization-file.dart').existsSync(), isTrue); + expect(outputDirectory.childFile('output-localization-file_en.dart').existsSync(), isTrue); + expect(outputDirectory.childFile('output-localization-file_es.dart').existsSync(), isTrue); + }, + ); - test('correctly creates output directory if it does not exist and writes files in it', () { - _standardFlutterDirectoryL10nSetup(fs); + test( + 'correctly creates output directory if it does not exist and writes files ' + 'in it while not using the synthetic package option', + () { + _standardFlutterDirectoryL10nSetup(fs); - LocalizationsGenerator generator; - try { - generator = LocalizationsGenerator(fs); - generator - ..initialize( - inputPathString: defaultL10nPathString, - outputPathString: path.join('lib', 'l10n', 'output'), - templateArbFileName: defaultTemplateArbFileName, - outputFileString: defaultOutputFileString, - classNameString: defaultClassNameString, - ) - ..loadResources() - ..writeOutputFiles(); - } on L10nException catch (e) { - fail('Generating output should not fail: \n${e.message}'); - } + LocalizationsGenerator generator; + try { + generator = LocalizationsGenerator(fs); + generator + ..initialize( + inputPathString: defaultL10nPathString, + outputPathString: path.join('lib', 'l10n', 'output'), + templateArbFileName: defaultTemplateArbFileName, + outputFileString: defaultOutputFileString, + classNameString: defaultClassNameString, + useSyntheticPackage: false, + ) + ..loadResources() + ..writeOutputFiles(); + } on L10nException catch (e) { + fail('Generating output should not fail: \n${e.message}'); + } - final Directory outputDirectory = fs.directory('lib').childDirectory('l10n').childDirectory('output'); - expect(outputDirectory.existsSync(), isTrue); - expect(outputDirectory.childFile('output-localization-file.dart').existsSync(), isTrue); - expect(outputDirectory.childFile('output-localization-file_en.dart').existsSync(), isTrue); - expect(outputDirectory.childFile('output-localization-file_es.dart').existsSync(), isTrue); - }); + final Directory outputDirectory = fs.directory('lib').childDirectory('l10n').childDirectory('output'); + expect(outputDirectory.existsSync(), isTrue); + expect(outputDirectory.childFile('output-localization-file.dart').existsSync(), isTrue); + expect(outputDirectory.childFile('output-localization-file_en.dart').existsSync(), isTrue); + expect(outputDirectory.childFile('output-localization-file_es.dart').existsSync(), isTrue); + }, + ); test('creates list of inputs and outputs when file path is specified', () { _standardFlutterDirectoryL10nSetup(fs); @@ -592,7 +618,7 @@ void main() { templateArbFileName: defaultTemplateArbFileName, outputFileString: defaultOutputFileString, classNameString: defaultClassNameString, - inputsAndOutputsListPath: defaultL10nPathString, + inputsAndOutputsListPath: syntheticPackagePath, ) ..loadResources() ..writeOutputFiles(); @@ -600,10 +626,9 @@ void main() { fail('Generating output should not fail: \n${e.message}'); } - final File inputsAndOutputsList = fs - .directory('lib') - .childDirectory('l10n') - .childFile('gen_l10n_inputs_and_outputs.json'); + final File inputsAndOutputsList = fs.file( + path.join(syntheticPackagePath, 'gen_l10n_inputs_and_outputs.json'), + ); expect(inputsAndOutputsList.existsSync(), isTrue); final Map jsonResult = json.decode(inputsAndOutputsList.readAsStringSync()) as Map; @@ -614,9 +639,9 @@ void main() { expect(jsonResult.containsKey('outputs'), isTrue); final List outputList = jsonResult['outputs'] as List; - expect(outputList, contains(fs.path.absolute('lib', 'l10n', 'output-localization-file.dart'))); - expect(outputList, contains(fs.path.absolute('lib', 'l10n', 'output-localization-file_en.dart'))); - expect(outputList, contains(fs.path.absolute('lib', 'l10n', 'output-localization-file_es.dart'))); + expect(outputList, contains(fs.path.absolute(syntheticPackagePath, 'output-localization-file.dart'))); + expect(outputList, contains(fs.path.absolute(syntheticPackagePath, 'output-localization-file_en.dart'))); + expect(outputList, contains(fs.path.absolute(syntheticPackagePath, 'output-localization-file_es.dart'))); }); test('setting both a headerString and a headerFile should fail', () { @@ -1105,11 +1130,11 @@ void main() { fail('Generating output files should not fail: $e'); } - expect(fs.isFileSync(path.join('lib', 'l10n', 'output-localization-file_en.dart')), true); - expect(fs.isFileSync(path.join('lib', 'l10n', 'output-localization-file_en_US.dart')), false); + expect(fs.isFileSync(path.join(syntheticPackagePath, 'output-localization-file_en.dart')), true); + expect(fs.isFileSync(path.join(syntheticPackagePath, 'output-localization-file_en_US.dart')), false); final String englishLocalizationsFile = fs.file( - path.join('lib', 'l10n', 'output-localization-file_en.dart') + path.join(syntheticPackagePath, 'output-localization-file_en.dart') ).readAsStringSync(); expect(englishLocalizationsFile, contains('class AppLocalizationsEnCa extends AppLocalizationsEn')); expect(englishLocalizationsFile, contains('class AppLocalizationsEn extends AppLocalizations')); @@ -1139,7 +1164,7 @@ void main() { } final String localizationsFile = fs.file( - path.join('lib', 'l10n', defaultOutputFileString), + path.join(syntheticPackagePath, defaultOutputFileString), ).readAsStringSync(); expect(localizationsFile, contains( ''' @@ -1157,7 +1182,6 @@ import 'output-localization-file_zh.dart'; try { generator.initialize( inputPathString: defaultL10nPathString, - outputPathString: defaultL10nPathString, templateArbFileName: defaultTemplateArbFileName, outputFileString: defaultOutputFileString, classNameString: defaultClassNameString, @@ -1170,7 +1194,7 @@ import 'output-localization-file_zh.dart'; } final String localizationsFile = fs.file( - path.join('lib', 'l10n', defaultOutputFileString), + path.join(syntheticPackagePath, defaultOutputFileString), ).readAsStringSync(); expect(localizationsFile, contains( ''' @@ -1204,7 +1228,6 @@ import 'output-localization-file_en.dart' deferred as output-localization-file_e try { generator.initialize( inputPathString: defaultL10nPathString, - outputPathString: defaultL10nPathString, templateArbFileName: defaultTemplateArbFileName, outputFileString: defaultOutputFileString, classNameString: defaultClassNameString, @@ -1583,7 +1606,6 @@ import 'output-localization-file_en.dart' deferred as output-localization-file_e try { generator.initialize( inputPathString: defaultL10nPathString, - outputPathString: defaultL10nPathString, templateArbFileName: defaultTemplateArbFileName, outputFileString: defaultOutputFileString, classNameString: defaultClassNameString, diff --git a/packages/flutter_tools/lib/src/build_system/targets/localizations.dart b/packages/flutter_tools/lib/src/build_system/targets/localizations.dart index 166766e3136..61b568d77a5 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/localizations.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/localizations.dart @@ -12,6 +12,7 @@ import '../../base/io.dart'; import '../../base/logger.dart'; import '../../convert.dart'; import '../../globals.dart' as globals; +import '../../project.dart'; import '../build_system.dart'; import '../depfile.dart'; @@ -37,6 +38,21 @@ Future generateLocalizations({ 'gen_l10n.dart', ); + // If generating a synthetic package, generate a warning if + // flutter: generate is not set. + final FlutterProject flutterProject = FlutterProject.fromDirectory(projectDir); + if (options.useSyntheticPackage && !flutterProject.manifest.generateSyntheticPackage) { + logger.printError( + 'Attempted to generate localizations code without having ' + 'the flutter: generate flag turned on.' + '\n' + 'Check pubspec.yaml and ensure that flutter: generate: true has ' + 'been added and rebuild the project. Otherwise, the localizations ' + 'source code will not be importable.' + ); + throw Exception(); + } + final ProcessResult result = await processManager.run([ dartBinaryPath, '--disable-dart-dev', @@ -61,6 +77,8 @@ Future generateLocalizations({ '--use-deferred-loading', if (options.preferredSupportedLocales != null) '--preferred-supported-locales=${options.preferredSupportedLocales}', + if (!options.useSyntheticPackage) + '--no-synthetic-package' ]); if (result.exitCode != 0) { logger.printError(result.stdout + result.stderr as String); @@ -157,7 +175,8 @@ class LocalizationOptions { this.preferredSupportedLocales, this.headerFile, this.deferredLoading, - }); + this.useSyntheticPackage = true, + }) : assert(useSyntheticPackage != null); /// The `--arb-dir` argument. /// @@ -201,6 +220,12 @@ class LocalizationOptions { /// Whether to generate the Dart localization file with locales imported /// as deferred. final bool deferredLoading; + + /// The `--synthetic-package` argument. + /// + /// Whether to generate the Dart localization files in a synthetic package + /// or in a custom directory. + final bool useSyntheticPackage; } /// Parse the localizations configuration options from [file]. @@ -232,6 +257,7 @@ LocalizationOptions parseLocalizationsOptions({ preferredSupportedLocales: _tryReadString(yamlMap, 'preferred-supported-locales', logger), headerFile: _tryReadUri(yamlMap, 'header-file', logger), deferredLoading: _tryReadBool(yamlMap, 'use-deferred-loading', logger), + useSyntheticPackage: _tryReadBool(yamlMap, 'synthetic-package', logger) ?? true, ); } diff --git a/packages/flutter_tools/lib/src/commands/packages.dart b/packages/flutter_tools/lib/src/commands/packages.dart index 65677d53030..2ae8d468bad 100644 --- a/packages/flutter_tools/lib/src/commands/packages.dart +++ b/packages/flutter_tools/lib/src/commands/packages.dart @@ -6,6 +6,10 @@ import 'dart:async'; import '../base/common.dart'; import '../base/os.dart'; +import '../build_info.dart'; +import '../build_system/build_system.dart'; +import '../cache.dart'; +import '../dart/generate_synthetic_packages.dart'; import '../dart/pub.dart'; import '../globals.dart' as globals; import '../project.dart'; @@ -89,9 +93,29 @@ class PackagesGetCommand extends FlutterCommand { } Future _runPubGet(String directory, FlutterProject flutterProject) async { + if (flutterProject.manifest.generateSyntheticPackage) { + final Environment 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: flutterProject.directory, + ); + + await generateLocalizationsSyntheticPackage( + environment: environment, + buildSystem: globals.buildSystem, + ); + } + final Stopwatch pubGetTimer = Stopwatch()..start(); try { - await pub.get(context: PubContext.pubGet, + await pub.get( + context: PubContext.pubGet, directory: directory, upgrade: upgrade , offline: boolArg('offline'), diff --git a/packages/flutter_tools/lib/src/commands/test.dart b/packages/flutter_tools/lib/src/commands/test.dart index a990b398883..f8ba3bb54e2 100644 --- a/packages/flutter_tools/lib/src/commands/test.dart +++ b/packages/flutter_tools/lib/src/commands/test.dart @@ -9,8 +9,10 @@ import '../asset.dart'; import '../base/common.dart'; import '../base/file_system.dart'; import '../build_info.dart'; +import '../build_system/build_system.dart'; import '../bundle.dart'; import '../cache.dart'; +import '../dart/generate_synthetic_packages.dart'; import '../dart/pub.dart'; import '../devfs.dart'; import '../globals.dart' as globals; @@ -164,6 +166,25 @@ class TestCommand extends FlutterCommand { } final FlutterProject flutterProject = FlutterProject.current(); if (shouldRunPub) { + if (flutterProject.manifest.generateSyntheticPackage) { + final Environment 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: flutterProject.directory, + ); + + await generateLocalizationsSyntheticPackage( + environment: environment, + buildSystem: globals.buildSystem, + ); + } + await pub.get( context: PubContext.getVerifyContext(name), skipPubspecYamlCheck: true, diff --git a/packages/flutter_tools/lib/src/dart/generate_synthetic_packages.dart b/packages/flutter_tools/lib/src/dart/generate_synthetic_packages.dart new file mode 100644 index 00000000000..2803eead072 --- /dev/null +++ b/packages/flutter_tools/lib/src/dart/generate_synthetic_packages.dart @@ -0,0 +1,72 @@ +// 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:yaml/yaml.dart'; + +import '../base/common.dart'; +import '../base/file_system.dart'; +import '../build_system/build_system.dart'; +import '../build_system/targets/localizations.dart'; + +Future generateLocalizationsSyntheticPackage({ + @required Environment environment, + @required BuildSystem buildSystem, +}) async { + assert(environment != null); + assert(buildSystem != null); + + final FileSystem fileSystem = environment.fileSystem; + final File l10nYamlFile = fileSystem.file( + fileSystem.path.join(environment.projectDir.path, 'l10n.yaml')); + + // If pubspec.yaml has generate:true and if l10n.yaml exists in the + // root project directory, check to see if a synthetic package should + // be generated for gen_l10n. + if (!l10nYamlFile.existsSync()) { + return; + } + + final YamlNode yamlNode = loadYamlNode(l10nYamlFile.readAsStringSync()); + if (yamlNode.value != null && yamlNode is! YamlMap) { + throwToolExit( + 'Expected ${l10nYamlFile.path} to contain a map, instead was $yamlNode' + ); + } + + BuildResult result; + // If an l10n.yaml file exists but is empty, attempt to build synthetic + // package with default settings. + if (yamlNode.value == null) { + result = await buildSystem.build( + const GenerateLocalizationsTarget(), + environment, + ); + } else { + final YamlMap yamlMap = yamlNode as YamlMap; + final Object value = yamlMap['synthetic-package']; + if (value is! bool && value != null) { + throwToolExit( + 'Expected "synthetic-package" to have a bool value, ' + 'instead was "$value"' + ); + } + + // Generate gen_l10n synthetic package only if synthetic-package: true or + // synthetic-package is null. + final bool isSyntheticL10nPackage = value as bool ?? true; + if (!isSyntheticL10nPackage) { + return; + } + } + + result = await buildSystem.build( + const GenerateLocalizationsTarget(), + environment, + ); + + if (result == null || result.hasException) { + throwToolExit('Generating synthetic localizations package has failed.'); + } +} diff --git a/packages/flutter_tools/lib/src/dart/pub.dart b/packages/flutter_tools/lib/src/dart/pub.dart index c69013d62ef..69c079cfb82 100644 --- a/packages/flutter_tools/lib/src/dart/pub.dart +++ b/packages/flutter_tools/lib/src/dart/pub.dart @@ -181,7 +181,8 @@ class _DefaultPub implements Pub { _fileSystem.path.join(directory, 'pubspec.yaml')); final File packageConfigFile = _fileSystem.file( _fileSystem.path.join(directory, '.dart_tool', 'package_config.json')); - final Directory generatedDirectory = _fileSystem.directory(_fileSystem.path.join(directory, '.dart_tool', 'flutter_gen')); + final Directory generatedDirectory = _fileSystem.directory( + _fileSystem.path.join(directory, '.dart_tool', 'flutter_gen')); if (!skipPubspecYamlCheck && !pubSpecYaml.existsSync()) { if (!skipIfAbsent) { diff --git a/packages/flutter_tools/lib/src/project.dart b/packages/flutter_tools/lib/src/project.dart index c91dfa6e88f..e5e654d4473 100644 --- a/packages/flutter_tools/lib/src/project.dart +++ b/packages/flutter_tools/lib/src/project.dart @@ -84,11 +84,11 @@ class FlutterProject { /// Returns a [FlutterProject] view of the current directory or a ToolExit error, /// if `pubspec.yaml` or `example/pubspec.yaml` is invalid. - static FlutterProject current() => fromDirectory(globals.fs.currentDirectory); + static FlutterProject current() => globals.projectFactory.fromDirectory(globals.fs.currentDirectory); /// Returns a [FlutterProject] view of the given directory or a ToolExit error, /// if `pubspec.yaml` or `example/pubspec.yaml` is invalid. - static FlutterProject fromPath(String path) => fromDirectory(globals.fs.directory(path)); + static FlutterProject fromPath(String path) => globals.projectFactory.fromDirectory(globals.fs.directory(path)); /// The location of this project. final Directory directory; diff --git a/packages/flutter_tools/lib/src/runner/flutter_command.dart b/packages/flutter_tools/lib/src/runner/flutter_command.dart index 2100e939134..fef5beb565e 100644 --- a/packages/flutter_tools/lib/src/runner/flutter_command.dart +++ b/packages/flutter_tools/lib/src/runner/flutter_command.dart @@ -18,9 +18,11 @@ import '../base/terminal.dart'; import '../base/user_messages.dart'; import '../base/utils.dart'; import '../build_info.dart'; +import '../build_system/build_system.dart'; import '../build_system/targets/icon_tree_shaker.dart' show kIconTreeShakerEnabledDefault; import '../bundle.dart' as bundle; import '../cache.dart'; +import '../dart/generate_synthetic_packages.dart'; import '../dart/package_map.dart'; import '../dart/pub.dart'; import '../device.dart'; @@ -886,6 +888,23 @@ abstract class FlutterCommand extends Command { if (shouldRunPub) { final FlutterProject project = FlutterProject.current(); + final Environment 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: project.directory, + ); + + await generateLocalizationsSyntheticPackage( + environment: environment, + buildSystem: globals.buildSystem, + ); + await pub.get( context: PubContext.getVerifyContext(name), generateSyntheticPackage: project.manifest.generateSyntheticPackage, 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 index 2482b965712..e0d79ef8fc5 100644 --- 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 @@ -14,7 +14,7 @@ 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 { + testUsingContext('generateLocalizations forwards arguments correctly', () async { final FileSystem fileSystem = MemoryFileSystem.test(); final Logger logger = BufferLogger.test(); final String projectDir = fileSystem.path.join('path', 'to', 'flutter_project'); @@ -34,7 +34,8 @@ void main() { '--header-file=header', '--header=HEADER', '--use-deferred-loading', - '--preferred-supported-locales=en_US' + '--preferred-supported-locales=en_US', + '--no-synthetic-package', ], ), ]); @@ -57,6 +58,7 @@ void main() { preferredSupportedLocales: 'en_US', templateArbFile: Uri.file('example.arb'), untranslatedMessagesFile: Uri.file('untranslated'), + useSyntheticPackage: false, ); await generateLocalizations( options: options, @@ -72,6 +74,54 @@ void main() { expect(processManager.hasRemainingExpectations, false); }); + testUsingContext('generateLocalizations throws exception on missing flutter: generate: true flag', () async { + final FileSystem fileSystem = MemoryFileSystem.test(); + final BufferLogger logger = BufferLogger.test(); + final FakeProcessManager processManager = FakeProcessManager.list([]); + final Directory arbDirectory = fileSystem.directory('arb') + ..createSync(); + arbDirectory.childFile('foo.arb').createSync(); + arbDirectory.childFile('bar.arb').createSync(); + + // Missing flutter: generate: true should throw exception. + fileSystem.file('pubspec.yaml').writeAsStringSync(''' +flutter: + uses-material-design: true +'''); + + 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'), + // Set synthetic package to true. + useSyntheticPackage: true, + ); + + expect( + () => generateLocalizations( + options: options, + logger: logger, + fileSystem: fileSystem, + processManager: processManager, + projectDir: fileSystem.currentDirectory, + dartBinaryPath: 'dart', + flutterRoot: '', + dependenciesDir: fileSystem.currentDirectory, + ), + throwsA(isA()), + ); + expect( + logger.errorText, + contains('Attempted to generate localizations code without having the flutter: generate flag turned on.'), + ); + }); + testWithoutContext('generateLocalizations is skipped if l10n.yaml does not exist.', () async { final FileSystem fileSystem = MemoryFileSystem.test(); final Environment environment = Environment.test( diff --git a/packages/flutter_tools/test/general.shard/dart/generate_synthetic_packages_test.dart b/packages/flutter_tools/test/general.shard/dart/generate_synthetic_packages_test.dart new file mode 100644 index 00000000000..1bf633a7a4c --- /dev/null +++ b/packages/flutter_tools/test/general.shard/dart/generate_synthetic_packages_test.dart @@ -0,0 +1,260 @@ +// 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/artifacts.dart'; +import 'package:flutter_tools/src/base/file_system.dart'; +import 'package:flutter_tools/src/base/logger.dart'; +import 'package:flutter_tools/src/dart/generate_synthetic_packages.dart'; +import 'package:flutter_tools/src/build_system/build_system.dart'; +import 'package:flutter_tools/src/build_system/targets/localizations.dart'; +import 'package:mockito/mockito.dart'; + +import '../../src/common.dart'; +import '../../src/context.dart'; +import '../../src/fake_process_manager.dart'; + +void main() { + testWithoutContext('calls buildSystem.build with blank l10n.yaml file', () { + // Project directory setup for gen_l10n logic + final MemoryFileSystem fileSystem = MemoryFileSystem.test(); + + // Add generate:true to pubspec.yaml. + final File pubspecFile = fileSystem.file('pubspec.yaml')..createSync(); + final String content = pubspecFile.readAsStringSync().replaceFirst( + '\nflutter:\n', + '\nflutter:\n generate: true\n', + ); + pubspecFile.writeAsStringSync(content); + + // Create an l10n.yaml file + fileSystem.file('l10n.yaml').createSync(); + + final FakeProcessManager mockProcessManager = FakeProcessManager.any(); + final BufferLogger mockBufferLogger = BufferLogger.test(); + final Artifacts mockArtifacts = Artifacts.test(); + final Environment environment = Environment.test( + fileSystem.currentDirectory, + fileSystem: fileSystem, + logger: mockBufferLogger, + artifacts: mockArtifacts, + processManager: mockProcessManager, + ); + final BuildSystem buildSystem = MockBuildSystem(); + + expect( + () => generateLocalizationsSyntheticPackage( + environment: environment, + buildSystem: buildSystem, + ), + throwsToolExit(message: 'Generating synthetic localizations package has failed.'), + ); + // [BuildSystem] should have called build with [GenerateLocalizationsTarget]. + verify(buildSystem.build( + const GenerateLocalizationsTarget(), + environment, + )).called(1); + }); + + testWithoutContext('calls buildSystem.build with l10n.yaml synthetic-package: true', () { + // Project directory setup for gen_l10n logic + final MemoryFileSystem fileSystem = MemoryFileSystem.test(); + + // Add generate:true to pubspec.yaml. + final File pubspecFile = fileSystem.file('pubspec.yaml')..createSync(); + final String content = pubspecFile.readAsStringSync().replaceFirst( + '\nflutter:\n', + '\nflutter:\n generate: true\n', + ); + pubspecFile.writeAsStringSync(content); + + // Create an l10n.yaml file + fileSystem.file('l10n.yaml').writeAsStringSync('synthetic-package: true'); + + final FakeProcessManager mockProcessManager = FakeProcessManager.any(); + final BufferLogger mockBufferLogger = BufferLogger.test(); + final Artifacts mockArtifacts = Artifacts.test(); + final Environment environment = Environment.test( + fileSystem.currentDirectory, + fileSystem: fileSystem, + logger: mockBufferLogger, + artifacts: mockArtifacts, + processManager: mockProcessManager, + ); + final BuildSystem buildSystem = MockBuildSystem(); + + expect( + () => generateLocalizationsSyntheticPackage( + environment: environment, + buildSystem: buildSystem, + ), + throwsToolExit(message: 'Generating synthetic localizations package has failed.'), + ); + // [BuildSystem] should have called build with [GenerateLocalizationsTarget]. + verify(buildSystem.build( + const GenerateLocalizationsTarget(), + environment, + )).called(1); + }); + + testWithoutContext('calls buildSystem.build with l10n.yaml synthetic-package: null', () { + // Project directory setup for gen_l10n logic + final MemoryFileSystem fileSystem = MemoryFileSystem.test(); + + // Add generate:true to pubspec.yaml. + final File pubspecFile = fileSystem.file('pubspec.yaml')..createSync(); + final String content = pubspecFile.readAsStringSync().replaceFirst( + '\nflutter:\n', + '\nflutter:\n generate: true\n', + ); + pubspecFile.writeAsStringSync(content); + + // Create an l10n.yaml file + fileSystem.file('l10n.yaml').writeAsStringSync('synthetic-package: null'); + + final FakeProcessManager mockProcessManager = FakeProcessManager.any(); + final BufferLogger mockBufferLogger = BufferLogger.test(); + final Artifacts mockArtifacts = Artifacts.test(); + final Environment environment = Environment.test( + fileSystem.currentDirectory, + fileSystem: fileSystem, + logger: mockBufferLogger, + artifacts: mockArtifacts, + processManager: mockProcessManager, + ); + final BuildSystem buildSystem = MockBuildSystem(); + + expect( + () => generateLocalizationsSyntheticPackage( + environment: environment, + buildSystem: buildSystem, + ), + throwsToolExit(message: 'Generating synthetic localizations package has failed.'), + ); + // [BuildSystem] should have called build with [GenerateLocalizationsTarget]. + verify(buildSystem.build( + const GenerateLocalizationsTarget(), + environment, + )).called(1); + }); + + testWithoutContext('does not call buildSystem.build when l10n.yaml is not present', () async { + // Project directory setup for gen_l10n logic + final MemoryFileSystem fileSystem = MemoryFileSystem.test(); + + // Add generate:true to pubspec.yaml. + final File pubspecFile = fileSystem.file('pubspec.yaml')..createSync(); + final String content = pubspecFile.readAsStringSync().replaceFirst( + '\nflutter:\n', + '\nflutter:\n generate: true\n', + ); + pubspecFile.writeAsStringSync(content); + + final FakeProcessManager mockProcessManager = FakeProcessManager.any(); + final BufferLogger mockBufferLogger = BufferLogger.test(); + final Artifacts mockArtifacts = Artifacts.test(); + final Environment environment = Environment.test( + fileSystem.currentDirectory, + fileSystem: fileSystem, + logger: mockBufferLogger, + artifacts: mockArtifacts, + processManager: mockProcessManager, + ); + final BuildSystem buildSystem = MockBuildSystem(); + + await generateLocalizationsSyntheticPackage( + environment: environment, + buildSystem: buildSystem, + ); + // [BuildSystem] should not be called with [GenerateLocalizationsTarget]. + verifyNever(buildSystem.build( + const GenerateLocalizationsTarget(), + environment, + )); + }); + + testWithoutContext('does not call buildSystem.build with incorrect l10n.yaml format', () { + // Project directory setup for gen_l10n logic + final MemoryFileSystem fileSystem = MemoryFileSystem.test(); + + // Add generate:true to pubspec.yaml. + final File pubspecFile = fileSystem.file('pubspec.yaml')..createSync(); + final String content = pubspecFile.readAsStringSync().replaceFirst( + '\nflutter:\n', + '\nflutter:\n generate: true\n', + ); + pubspecFile.writeAsStringSync(content); + + // Create an l10n.yaml file + fileSystem.file('l10n.yaml').writeAsStringSync('helloWorld'); + + final FakeProcessManager mockProcessManager = FakeProcessManager.any(); + final BufferLogger mockBufferLogger = BufferLogger.test(); + final Artifacts mockArtifacts = Artifacts.test(); + final Environment environment = Environment.test( + fileSystem.currentDirectory, + fileSystem: fileSystem, + logger: mockBufferLogger, + artifacts: mockArtifacts, + processManager: mockProcessManager, + ); + final BuildSystem buildSystem = MockBuildSystem(); + + expect( + () => generateLocalizationsSyntheticPackage( + environment: environment, + buildSystem: buildSystem, + ), + throwsToolExit(message: 'to contain a map, instead was helloWorld'), + ); + // [BuildSystem] should not be called with [GenerateLocalizationsTarget]. + verifyNever(buildSystem.build( + const GenerateLocalizationsTarget(), + environment, + )); + }); + + testWithoutContext('does not call buildSystem.build with non-bool "synthetic-package" value', () { + // Project directory setup for gen_l10n logic + final MemoryFileSystem fileSystem = MemoryFileSystem.test(); + + // Add generate:true to pubspec.yaml. + final File pubspecFile = fileSystem.file('pubspec.yaml')..createSync(); + final String content = pubspecFile.readAsStringSync().replaceFirst( + '\nflutter:\n', + '\nflutter:\n generate: true\n', + ); + pubspecFile.writeAsStringSync(content); + + // Create an l10n.yaml file + fileSystem.file('l10n.yaml').writeAsStringSync('synthetic-package: nonBoolValue'); + + final FakeProcessManager mockProcessManager = FakeProcessManager.any(); + final BufferLogger mockBufferLogger = BufferLogger.test(); + final Artifacts mockArtifacts = Artifacts.test(); + final Environment environment = Environment.test( + fileSystem.currentDirectory, + fileSystem: fileSystem, + logger: mockBufferLogger, + artifacts: mockArtifacts, + processManager: mockProcessManager, + ); + final BuildSystem buildSystem = MockBuildSystem(); + + expect( + () => generateLocalizationsSyntheticPackage( + environment: environment, + buildSystem: buildSystem, + ), + throwsToolExit(message: 'to have a bool value, instead was "nonBoolValue"'), + ); + // [BuildSystem] should not be called with [GenerateLocalizationsTarget]. + verifyNever(buildSystem.build( + const GenerateLocalizationsTarget(), + environment, + )); + }); +} + +class MockBuildSystem extends Mock implements BuildSystem {} 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 2726bf86249..46fa510cedf 100644 --- a/packages/flutter_tools/test/general.shard/resident_runner_test.dart +++ b/packages/flutter_tools/test/general.shard/resident_runner_test.dart @@ -1201,6 +1201,7 @@ void main() { globals.fs.file(globals.fs.path.join('lib', 'l10n', 'foo.arb')) .createSync(recursive: true); globals.fs.file('l10n.yaml').createSync(); + globals.fs.file('pubspec.yaml').writeAsStringSync('flutter:\n generate: true\n'); await residentRunner.runSourceGenerators(); @@ -1227,6 +1228,7 @@ void main() { globals.fs.file(globals.fs.path.join('lib', 'l10n', 'foo.arb')) .createSync(recursive: true); globals.fs.file('l10n.yaml').createSync(); + globals.fs.file('pubspec.yaml').writeAsStringSync('flutter:\n generate: true\n'); await residentRunner.runSourceGenerators(); 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 103d7007565..ab453e3079c 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 @@ -16,6 +16,7 @@ class GenL10nProject extends Project { @override Future setUpIn(Directory dir, { bool useDeferredLoading = false, + bool useSyntheticPackage = false, }) { this.dir = dir; writeFile(globals.fs.path.join(dir.path, 'lib', 'l10n', 'app_en.arb'), appEn); @@ -27,7 +28,10 @@ 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)); + writeFile(globals.fs.path.join(dir.path, 'l10n.yaml'), l10nYaml( + useDeferredLoading: useDeferredLoading, + useSyntheticPackage: useSyntheticPackage, + )); return super.setUpIn(dir); } @@ -568,12 +572,18 @@ void main() { String l10nYaml({ @required bool useDeferredLoading, + @required bool useSyntheticPackage, }) { + String l10nYamlString = ''; + if (useDeferredLoading) { - return r''' -use-deferred-loading: false - '''; + l10nYamlString += 'use-deferred-loading: true\n'; } - return ''; + + if (!useSyntheticPackage) { + l10nYamlString += 'synthetic-package: false\n'; + } + + return l10nYamlString; } }