From 63ff7a199b86fd483b6a30b7ff7457a96b7bf06c Mon Sep 17 00:00:00 2001 From: Gary Qian Date: Wed, 16 Mar 2022 11:10:22 -0700 Subject: [PATCH] MigrateConfig and migrate integration testing base (#99092) --- .../lib/src/commands/create.dart | 8 + .../lib/src/commands/create_base.dart | 78 ++++- .../lib/src/flutter_project_metadata.dart | 328 +++++++++++++++--- packages/flutter_tools/lib/src/project.dart | 55 ++- .../flutter_tools/lib/src/xcode_project.dart | 6 + .../templates/app_shared/.gitignore.tmpl | 1 + .../templates/app_shared/.metadata.tmpl | 10 - .../templates/module/common/.gitignore.tmpl | 2 + .../templates/package/.gitignore.tmpl | 1 + .../templates/plugin_shared/.gitignore.tmpl | 1 + .../hermetic/create_usage_test.dart | 10 +- .../flutter_project_metadata_test.dart | 102 +++++- .../migrate_config_test.dart | 231 ++++++++++++ .../vanilla_app_1_22_6_stable.ensure | 1 + .../test_data/migrate_project.dart | 237 +++++++++++++ .../test/integration.shard/test_utils.dart | 2 +- 16 files changed, 982 insertions(+), 91 deletions(-) delete mode 100644 packages/flutter_tools/templates/app_shared/.metadata.tmpl create mode 100644 packages/flutter_tools/test/integration.shard/migrate_config_test.dart create mode 100644 packages/flutter_tools/test/integration.shard/test_data/full_apps/vanilla_app_1_22_6_stable.ensure create mode 100644 packages/flutter_tools/test/integration.shard/test_data/migrate_project.dart diff --git a/packages/flutter_tools/lib/src/commands/create.dart b/packages/flutter_tools/lib/src/commands/create.dart index 81731938ff..f0c7263297 100644 --- a/packages/flutter_tools/lib/src/commands/create.dart +++ b/packages/flutter_tools/lib/src/commands/create.dart @@ -320,6 +320,7 @@ class CreateCommand extends CreateBase { templateContext, overwrite: overwrite, printStatusWhenWriting: !creatingNewProject, + projectType: template, ); break; case FlutterProjectType.skeleton: @@ -329,6 +330,7 @@ class CreateCommand extends CreateBase { templateContext, overwrite: overwrite, printStatusWhenWriting: !creatingNewProject, + generateMetadata: false, ); break; case FlutterProjectType.module: @@ -353,6 +355,7 @@ class CreateCommand extends CreateBase { templateContext, overwrite: overwrite, printStatusWhenWriting: !creatingNewProject, + projectType: template, ); break; case FlutterProjectType.ffiPlugin: @@ -361,6 +364,7 @@ class CreateCommand extends CreateBase { templateContext, overwrite: overwrite, printStatusWhenWriting: !creatingNewProject, + projectType: template, ); break; } @@ -495,6 +499,7 @@ Your $application code is in $relativeAppMain. Map templateContext, { bool overwrite = false, bool printStatusWhenWriting = true, + FlutterProjectType projectType, }) async { // Plugins only add a platform if it was requested explicitly by the user. if (!argResults.wasParsed('platforms')) { @@ -561,6 +566,7 @@ Your $application code is in $relativeAppMain. overwrite: overwrite, pluginExampleApp: true, printStatusWhenWriting: printStatusWhenWriting, + projectType: projectType, ); return generatedCount; } @@ -570,6 +576,7 @@ Your $application code is in $relativeAppMain. Map templateContext, { bool overwrite = false, bool printStatusWhenWriting = true, + FlutterProjectType projectType, }) async { // Plugins only add a platform if it was requested explicitly by the user. if (!argResults.wasParsed('platforms')) { @@ -637,6 +644,7 @@ Your $application code is in $relativeAppMain. overwrite: overwrite, pluginExampleApp: true, printStatusWhenWriting: printStatusWhenWriting, + projectType: projectType, ); return generatedCount; } diff --git a/packages/flutter_tools/lib/src/commands/create_base.dart b/packages/flutter_tools/lib/src/commands/create_base.dart index dfc2331f16..85109c39a1 100644 --- a/packages/flutter_tools/lib/src/commands/create_base.dart +++ b/packages/flutter_tools/lib/src/commands/create_base.dart @@ -134,6 +134,13 @@ abstract class CreateBase extends FlutterCommand { 'This is only intended to enable testing of the tool itself.', hide: !verboseHelp, ); + argParser.addOption( + 'initial-create-revision', + defaultsTo: null, + help: 'The Flutter SDK git commit hash to store in .migrate_config. This parameter is used by the tool ' + 'internally and should generally not be used manually.', + hide: !verboseHelp, + ); } /// The output directory of the command. @@ -488,6 +495,8 @@ abstract class CreateBase extends FlutterCommand { bool overwrite = false, bool pluginExampleApp = false, bool printStatusWhenWriting = true, + bool generateMetadata = true, + FlutterProjectType projectType, }) async { int generatedCount = 0; generatedCount += await renderMerged( @@ -502,6 +511,14 @@ abstract class CreateBase extends FlutterCommand { generatedCount += _injectGradleWrapper(project); } + final bool androidPlatform = templateContext['android'] as bool ?? false; + final bool iosPlatform = templateContext['ios'] as bool ?? false; + final bool linuxPlatform = templateContext['linux'] as bool ?? false; + final bool macOSPlatform = templateContext['macos'] as bool ?? false; + final bool windowsPlatform = templateContext['windows'] as bool ?? false; + final bool webPlatform = templateContext['web'] as bool ?? false; + final bool winUwpPlatform = templateContext['winuwp'] as bool ?? false; + if (boolArg('pub')) { final Environment environment = Environment( artifacts: globals.artifacts, @@ -534,18 +551,63 @@ abstract class CreateBase extends FlutterCommand { ); await project.ensureReadyForPlatformSpecificTooling( - androidPlatform: templateContext['android'] as bool ?? false, - iosPlatform: templateContext['ios'] as bool ?? false, - linuxPlatform: templateContext['linux'] as bool ?? false, - macOSPlatform: templateContext['macos'] as bool ?? false, - windowsPlatform: templateContext['windows'] as bool ?? false, - webPlatform: templateContext['web'] as bool ?? false, - winUwpPlatform: templateContext['winuwp'] as bool ?? false, + androidPlatform: androidPlatform, + iosPlatform: iosPlatform, + linuxPlatform: linuxPlatform, + macOSPlatform: macOSPlatform, + windowsPlatform: windowsPlatform, + webPlatform: webPlatform, + winUwpPlatform: winUwpPlatform, ); } - if (templateContext['android'] == true) { + final List platformsForMigrateConfig = [SupportedPlatform.root]; + if (androidPlatform) { gradle.updateLocalProperties(project: project, requireAndroidSdk: false); + platformsForMigrateConfig.add(SupportedPlatform.android); } + if (iosPlatform) { + platformsForMigrateConfig.add(SupportedPlatform.ios); + } + if (linuxPlatform) { + platformsForMigrateConfig.add(SupportedPlatform.linux); + } + if (macOSPlatform) { + platformsForMigrateConfig.add(SupportedPlatform.macos); + } + if (webPlatform) { + platformsForMigrateConfig.add(SupportedPlatform.web); + } + if (windowsPlatform) { + platformsForMigrateConfig.add(SupportedPlatform.windows); + } + if (winUwpPlatform) { + platformsForMigrateConfig.add(SupportedPlatform.windowsuwp); + } + if (templateContext['fuchsia'] == true) { + platformsForMigrateConfig.add(SupportedPlatform.fuchsia); + } + if (generateMetadata) { + final File metadataFile = globals.fs + .file(globals.fs.path.join(projectDir.absolute.path, '.metadata')); + final FlutterProjectMetadata metadata = FlutterProjectMetadata.explicit( + file: metadataFile, + versionRevision: globals.flutterVersion.frameworkRevision, + versionChannel: globals.flutterVersion.channel, + projectType: projectType, + migrateConfig: MigrateConfig(), + logger: globals.logger); + metadata.populate( + platforms: platformsForMigrateConfig, + projectDirectory: directory, + create: true, + update: false, + currentRevision: stringArg('initial-create-revision') ?? globals.flutterVersion.frameworkRevision, + createRevision: globals.flutterVersion.frameworkRevision, + logger: globals.logger, + ); + metadata.writeFile(); + } + return generatedCount; } diff --git a/packages/flutter_tools/lib/src/flutter_project_metadata.dart b/packages/flutter_tools/lib/src/flutter_project_metadata.dart index da2800fbf0..e2fcf2e901 100644 --- a/packages/flutter_tools/lib/src/flutter_project_metadata.dart +++ b/packages/flutter_tools/lib/src/flutter_project_metadata.dart @@ -7,6 +7,8 @@ import 'package:yaml/yaml.dart'; import 'base/file_system.dart'; import 'base/logger.dart'; import 'base/utils.dart'; +import 'project.dart'; +import 'version.dart'; enum FlutterProjectType { /// This is the default project with the user-managed host code. @@ -27,7 +29,10 @@ enum FlutterProjectType { ffiPlugin, } -String flutterProjectTypeToString(FlutterProjectType type) { +String flutterProjectTypeToString(FlutterProjectType? type) { + if (type == null) { + return ''; + } if (type == FlutterProjectType.ffiPlugin) { return 'plugin_ffi'; } @@ -45,67 +50,286 @@ FlutterProjectType? stringToProjectType(String value) { return result; } + /// Verifies the expected yaml keys are present in the file. + bool _validateMetadataMap(Object? yamlRoot, Map validations, Logger logger) { + if (yamlRoot != null && yamlRoot is! YamlMap) { + return false; + } + final YamlMap map = yamlRoot! as YamlMap; + bool isValid = true; + for (final MapEntry entry in validations.entries) { + if (!map.keys.contains(entry.key)) { + isValid = false; + logger.printTrace('The key `${entry.key}` was not found'); + break; + } + if (map[entry.key] != null && (map[entry.key] as Object).runtimeType != entry.value) { + isValid = false; + logger.printTrace('The value of key `${entry.key}` in .metadata was expected to be ${entry.value} but was ${(map[entry.key] as Object).runtimeType}'); + break; + } + } + return isValid; + } + /// A wrapper around the `.metadata` file. class FlutterProjectMetadata { - FlutterProjectMetadata( - File metadataFile, - Logger logger, - ) : _metadataFile = metadataFile, - _logger = logger; + /// Creates a MigrateConfig by parsing an existing .migrate_config yaml file. + FlutterProjectMetadata(File file, Logger logger) : _metadataFile = file, + _logger = logger, + migrateConfig = MigrateConfig() { + if (!_metadataFile.existsSync()) { + _logger.printTrace('No .metadata file found at ${_metadataFile.path}.'); + // Create a default empty metadata. + return; + } + Object? yamlRoot; + try { + yamlRoot = loadYaml(_metadataFile.readAsStringSync()); + } on YamlException { + // Handled in _validate below. + } + if (yamlRoot == null || yamlRoot is! YamlMap) { + _logger.printTrace('.metadata file at ${_metadataFile.path} was empty or malformed.'); + return; + } + final YamlMap map = yamlRoot; + if (_validateMetadataMap(yamlRoot, {'version': YamlMap}, _logger)) { + final Object? versionYaml = map['version']; + if (_validateMetadataMap(versionYaml, { + 'revision': String, + 'channel': String, + }, _logger)) { + final YamlMap versionYamlMap = versionYaml! as YamlMap; + _versionRevision = versionYamlMap['revision'] as String?; + _versionChannel = versionYamlMap['channel'] as String?; + } + } + if (_validateMetadataMap(yamlRoot, {'project_type': String}, _logger)) { + _projectType = stringToProjectType(map['project_type'] as String); + } + final Object? migrationYaml = map['migration']; + if (migrationYaml != null && migrationYaml is YamlMap) { + migrateConfig.parseYaml(map['migration'] as YamlMap, _logger); + } + } + + /// Creates a MigrateConfig by explicitly providing all values. + FlutterProjectMetadata.explicit({ + required File file, + required String? versionRevision, + required String? versionChannel, + required FlutterProjectType? projectType, + required this.migrateConfig, + required Logger logger, + }) : _logger = logger, + _versionChannel = versionChannel, + _versionRevision = versionRevision, + _projectType = projectType, + _metadataFile = file; + + /// The name of the config file. + static const String kFileName = '.metadata'; + + String? _versionRevision; + String? get versionRevision => _versionRevision; + + String? _versionChannel; + String? get versionChannel => _versionChannel; + + FlutterProjectType? _projectType; + FlutterProjectType? get projectType => _projectType; + + /// Metadata and configuration for the migrate command. + MigrateConfig migrateConfig; - final File _metadataFile; final Logger _logger; - String? get versionChannel => _versionValue('channel'); - String? get versionRevision => _versionValue('revision'); + final File _metadataFile; - FlutterProjectType? get projectType { - final dynamic projectTypeYaml = _metadataValue('project_type'); - if (projectTypeYaml is String) { - return stringToProjectType(projectTypeYaml); - } else { - _logger.printTrace('.metadata project_type version is malformed.'); - return null; - } + /// Writes the .migrate_config file in the provided project directory's platform subdirectory. + /// + /// We write the file manually instead of with a template because this + /// needs to be able to write the .migrate_config file into legacy apps. + void writeFile({File? outputFile}) { + outputFile = outputFile ?? _metadataFile; + outputFile + ..createSync(recursive: true) + ..writeAsStringSync(''' +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled. + +version: + revision: $_versionRevision + channel: $_versionChannel + +project_type: ${flutterProjectTypeToString(projectType)} +${migrateConfig.getOutputFileString()}''', + flush: true); } - YamlMap? _versionYaml; - String? _versionValue(String key) { - if (_versionYaml == null) { - final dynamic versionYaml = _metadataValue('version'); - if (versionYaml is YamlMap) { - _versionYaml = versionYaml; - } else { - _logger.printTrace('.metadata version is malformed.'); - return null; - } - } - if (_versionYaml != null && _versionYaml!.containsKey(key) && _versionYaml![key] is String) { - return _versionYaml![key] as String; - } - return null; + void populate({ + List? platforms, + Directory? projectDirectory, + String? currentRevision, + String? createRevision, + bool create = true, + bool update = true, + required Logger logger, + }) { + migrateConfig.populate( + platforms: platforms, + projectDirectory: projectDirectory, + currentRevision: currentRevision, + createRevision: createRevision, + create: create, + update: update, + logger: logger, + ); } - YamlMap? _metadataYaml; - dynamic _metadataValue(String key) { - if (_metadataYaml == null) { - if (!_metadataFile.existsSync()) { - return null; - } - dynamic metadataYaml; - try { - metadataYaml = loadYaml(_metadataFile.readAsStringSync()); - } on YamlException { - // Handled in return below. - } - if (metadataYaml is YamlMap) { - _metadataYaml = metadataYaml; - } else { - _logger.printTrace('.metadata is malformed.'); - return null; - } + /// Finds the fallback revision to use when no base revision is found in the migrate config. + String getFallbackBaseRevision(Logger logger, FlutterVersion flutterVersion) { + // Use the .metadata file if it exists. + if (versionRevision != null) { + return versionRevision!; } - - return _metadataYaml![key]; + return flutterVersion.frameworkRevision; } } + +/// Represents the migrate command metadata section of a .metadata file. +/// +/// This file tracks the flutter sdk git hashes of the last successful migration ('base') and +/// the version the project was created with. +/// +/// Each platform tracks a different set of revisions because flutter create can be +/// used to add support for new platforms, so the base and create revision may not always be the same. +class MigrateConfig { + MigrateConfig({ + Map? platformConfigs, + this.unmanagedFiles = _kDefaultUnmanagedFiles + }) : platformConfigs = platformConfigs ?? {}; + + /// A mapping of the files that are unmanaged by defult for each platform. + static const List _kDefaultUnmanagedFiles = [ + 'lib/main.dart', + 'ios/Runner.xcodeproj/project.pbxproj', + ]; + + /// The metadata for each platform supported by the project. + final Map platformConfigs; + + /// A list of paths relative to this file the migrate tool should ignore. + /// + /// These files are typically user-owned files that should not be changed. + List unmanagedFiles; + + bool get isEmpty => platformConfigs.isEmpty && (unmanagedFiles.isEmpty || unmanagedFiles == _kDefaultUnmanagedFiles); + + /// Parses the project for all supported platforms and populates the [MigrateConfig] + /// to reflect the project. + void populate({ + List? platforms, + Directory? projectDirectory, + String? currentRevision, + String? createRevision, + bool create = true, + bool update = true, + required Logger logger, + }) { + final FlutterProject flutterProject = projectDirectory == null ? FlutterProject.current() : FlutterProject.fromDirectory(projectDirectory); + platforms ??= flutterProject.getSupportedPlatforms(includeRoot: true); + + for (final SupportedPlatform platform in platforms) { + if (platformConfigs.containsKey(platform)) { + if (update) { + platformConfigs[platform]!.baseRevision = currentRevision; + } + } else { + if (create) { + platformConfigs[platform] = MigratePlatformConfig(createRevision: createRevision, baseRevision: currentRevision); + } + } + } + } + + /// Returns the string that should be written to the .metadata file. + String getOutputFileString() { + String unmanagedFilesString = ''; + for (final String path in unmanagedFiles) { + unmanagedFilesString += "\n - '$path'"; + } + + String platformsString = ''; + for (final MapEntry entry in platformConfigs.entries) { + platformsString += '\n - platform: ${entry.key.toString().split('.').last}\n create_revision: ${entry.value.createRevision == null ? 'null' : "${entry.value.createRevision}"}\n base_revision: ${entry.value.baseRevision == null ? 'null' : "${entry.value.baseRevision}"}'; + } + + return isEmpty ? '' : ''' + +# Tracks metadata for the flutter migrate command +migration: + platforms:$platformsString + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files:$unmanagedFilesString +'''; + } + + /// Parses and validates the `migration` section of the .metadata file. + void parseYaml(YamlMap map, Logger logger) { + final Object? platformsYaml = map['platforms']; + if (_validateMetadataMap(map, {'platforms': YamlList}, logger)) { + if (platformsYaml is YamlList && platformsYaml.isNotEmpty) { + for (final Object? platform in platformsYaml) { + if (_validateMetadataMap(platform, { + 'platform': String, + 'create_revision': String, + 'base_revision': String, + }, logger)) { + final YamlMap platformYamlMap = platform! as YamlMap; + final SupportedPlatform platformString = SupportedPlatform.values.firstWhere( + (SupportedPlatform val) => val.toString() == 'SupportedPlatform.${platformYamlMap['platform'] as String}' + ); + platformConfigs[platformString] = MigratePlatformConfig( + createRevision: platformYamlMap['create_revision'] as String?, + baseRevision: platformYamlMap['base_revision'] as String?, + ); + } else { + // malformed platform entry + continue; + } + } + } + } + if (_validateMetadataMap(map, {'unmanaged_files': YamlList}, logger)) { + final Object? unmanagedFilesYaml = map['unmanaged_files']; + if (unmanagedFilesYaml is YamlList && unmanagedFilesYaml.isNotEmpty) { + unmanagedFiles = List.from(unmanagedFilesYaml.value.cast()); + } + } + } +} + +/// Holds the revisions for a single platform for use by the flutter migrate command. +class MigratePlatformConfig { + MigratePlatformConfig({this.createRevision, this.baseRevision}); + + /// The Flutter SDK revision this platform was created by. + /// + /// Null if the initial create git revision is unknown. + final String? createRevision; + + /// The Flutter SDK revision this platform was last migrated by. + /// + /// Null if the project was never migrated or the revision is unknown. + String? baseRevision; +} diff --git a/packages/flutter_tools/lib/src/project.dart b/packages/flutter_tools/lib/src/project.dart index 52276c4ace..d56afe6270 100644 --- a/packages/flutter_tools/lib/src/project.dart +++ b/packages/flutter_tools/lib/src/project.dart @@ -27,6 +27,19 @@ import 'xcode_project.dart'; export 'cmake_project.dart'; export 'xcode_project.dart'; +/// Emum for each officially supported platform. +enum SupportedPlatform { + android, + ios, + linux, + macos, + web, + windows, + windowsuwp, + fuchsia, + root, // Special platform to represent the root project directory +} + class FlutterProjectFactory { FlutterProjectFactory({ required Logger logger, @@ -244,6 +257,36 @@ class FlutterProject { /// True if this project has an example application. bool get hasExampleApp => _exampleDirectory(directory).existsSync(); + /// Returns a list of platform names that are supported by the project. + List getSupportedPlatforms({bool includeRoot = false}) { + final List platforms = includeRoot ? [SupportedPlatform.root] : []; + if (android.existsSync()) { + platforms.add(SupportedPlatform.android); + } + if (ios.exists) { + platforms.add(SupportedPlatform.ios); + } + if (web.existsSync()) { + platforms.add(SupportedPlatform.web); + } + if (macos.existsSync()) { + platforms.add(SupportedPlatform.macos); + } + if (linux.existsSync()) { + platforms.add(SupportedPlatform.linux); + } + if (windows.existsSync()) { + platforms.add(SupportedPlatform.windows); + } + if (windowsUwp.existsSync()) { + platforms.add(SupportedPlatform.windowsuwp); + } + if (fuchsia.existsSync()) { + platforms.add(SupportedPlatform.fuchsia); + } + return platforms; + } + /// The directory that will contain the example if an example exists. static Directory _exampleDirectory(Directory directory) => directory.childDirectory('example'); @@ -555,12 +598,10 @@ class AndroidProject extends FlutterProjectPlatform { if (deprecationBehavior == DeprecationBehavior.none) { return; } - final AndroidEmbeddingVersionResult result = computeEmbeddingVersion(); if (result.version != AndroidEmbeddingVersion.v1) { return; } - globals.printStatus( ''' ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ @@ -584,11 +625,11 @@ The detected reason was: if (deprecationBehavior == DeprecationBehavior.ignore) { BuildEvent('deprecated-v1-android-embedding-ignored', type: 'gradle', flutterUsage: globals.flutterUsage).send(); } else { // DeprecationBehavior.exit - BuildEvent('deprecated-v1-android-embedding-failed', type: 'gradle', flutterUsage: globals.flutterUsage).send(); - throwToolExit( - 'Build failed due to use of deprecated Android v1 embedding.', - exitCode: 1, - ); + BuildEvent('deprecated-v1-android-embedding-failed', type: 'gradle', flutterUsage: globals.flutterUsage).send(); + throwToolExit( + 'Build failed due to use of deprecated Android v1 embedding.', + exitCode: 1, + ); } } diff --git a/packages/flutter_tools/lib/src/xcode_project.dart b/packages/flutter_tools/lib/src/xcode_project.dart index e6ef98f702..6faec24d27 100644 --- a/packages/flutter_tools/lib/src/xcode_project.dart +++ b/packages/flutter_tools/lib/src/xcode_project.dart @@ -134,6 +134,12 @@ class IosProject extends XcodeBasedProject { Directory get symlinks => _flutterLibRoot.childDirectory('.symlinks'); + /// True, if the app project is using swift. + bool get isSwift { + final File appDelegateSwift = _editableDirectory.childDirectory('Runner').childFile('AppDelegate.swift'); + return appDelegateSwift.existsSync(); + } + /// Do all plugins support arm64 simulators to run natively on an ARM Mac? Future pluginsSupportArmSimulator() async { final Directory podXcodeProject = hostAppRoot diff --git a/packages/flutter_tools/templates/app_shared/.gitignore.tmpl b/packages/flutter_tools/templates/app_shared/.gitignore.tmpl index 0fa6b675c0..a8e938c083 100644 --- a/packages/flutter_tools/templates/app_shared/.gitignore.tmpl +++ b/packages/flutter_tools/templates/app_shared/.gitignore.tmpl @@ -8,6 +8,7 @@ .buildlog/ .history .svn/ +migrate_working_dir/ # IntelliJ related *.iml diff --git a/packages/flutter_tools/templates/app_shared/.metadata.tmpl b/packages/flutter_tools/templates/app_shared/.metadata.tmpl deleted file mode 100644 index 98005dbbcb..0000000000 --- a/packages/flutter_tools/templates/app_shared/.metadata.tmpl +++ /dev/null @@ -1,10 +0,0 @@ -# This file tracks properties of this Flutter project. -# Used by Flutter tool to assess capabilities and perform upgrades etc. -# -# This file should be version controlled and should not be manually edited. - -version: - revision: {{flutterRevision}} - channel: {{flutterChannel}} - -project_type: app diff --git a/packages/flutter_tools/templates/module/common/.gitignore.tmpl b/packages/flutter_tools/templates/module/common/.gitignore.tmpl index ff612b3be4..9141595fb9 100644 --- a/packages/flutter_tools/templates/module/common/.gitignore.tmpl +++ b/packages/flutter_tools/templates/module/common/.gitignore.tmpl @@ -9,6 +9,8 @@ .sconsign.dblite .svn/ +migrate_working_dir/ + *.swp profile diff --git a/packages/flutter_tools/templates/package/.gitignore.tmpl b/packages/flutter_tools/templates/package/.gitignore.tmpl index 9be145fde9..96486fd930 100644 --- a/packages/flutter_tools/templates/package/.gitignore.tmpl +++ b/packages/flutter_tools/templates/package/.gitignore.tmpl @@ -8,6 +8,7 @@ .buildlog/ .history .svn/ +migrate_working_dir/ # IntelliJ related *.iml diff --git a/packages/flutter_tools/templates/plugin_shared/.gitignore.tmpl b/packages/flutter_tools/templates/plugin_shared/.gitignore.tmpl index 9be145fde9..96486fd930 100644 --- a/packages/flutter_tools/templates/plugin_shared/.gitignore.tmpl +++ b/packages/flutter_tools/templates/plugin_shared/.gitignore.tmpl @@ -8,6 +8,7 @@ .buildlog/ .history .svn/ +migrate_working_dir/ # IntelliJ related *.iml diff --git a/packages/flutter_tools/test/commands.shard/hermetic/create_usage_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/create_usage_test.dart index bb283a3198..b4a889c937 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/create_usage_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/create_usage_test.dart @@ -88,19 +88,19 @@ void main() { await runner.run(['create', '--no-pub', '--template=module', 'testy']); expect((await command.usageValues).commandCreateProjectType, 'module'); - await runner.run(['create', '--no-pub', '--template=app', 'testy']); + await runner.run(['create', '--no-pub', '--template=app', 'testy1']); expect((await command.usageValues).commandCreateProjectType, 'app'); - await runner.run(['create', '--no-pub', '--template=skeleton', 'testy']); + await runner.run(['create', '--no-pub', '--template=skeleton', 'testy2']); expect((await command.usageValues).commandCreateProjectType, 'skeleton'); - await runner.run(['create', '--no-pub', '--template=package', 'testy']); + await runner.run(['create', '--no-pub', '--template=package', 'testy3']); expect((await command.usageValues).commandCreateProjectType, 'package'); - await runner.run(['create', '--no-pub', '--template=plugin', 'testy']); + await runner.run(['create', '--no-pub', '--template=plugin', 'testy4']); expect((await command.usageValues).commandCreateProjectType, 'plugin'); - await runner.run(['create', '--no-pub', '--template=plugin_ffi', 'testy']); + await runner.run(['create', '--no-pub', '--template=plugin_ffi', 'testy5']); expect((await command.usageValues).commandCreateProjectType, 'plugin_ffi'); })); diff --git a/packages/flutter_tools/test/general.shard/flutter_project_metadata_test.dart b/packages/flutter_tools/test/general.shard/flutter_project_metadata_test.dart index 79a2f2d6a6..b5bfdd0c67 100644 --- a/packages/flutter_tools/test/general.shard/flutter_project_metadata_test.dart +++ b/packages/flutter_tools/test/general.shard/flutter_project_metadata_test.dart @@ -6,6 +6,7 @@ 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/flutter_project_metadata.dart'; +import 'package:flutter_tools/src/project.dart'; import '../src/common.dart'; @@ -26,8 +27,7 @@ void main() { expect(projectMetadata.versionChannel, isNull); expect(projectMetadata.versionRevision, isNull); - expect(logger.traceText, contains('.metadata project_type version is malformed.')); - expect(logger.traceText, contains('.metadata version is malformed.')); + expect(logger.traceText, contains('No .metadata file found at .metadata')); }); testWithoutContext('project metadata fields are empty when file is empty', () { @@ -37,8 +37,7 @@ void main() { expect(projectMetadata.versionChannel, isNull); expect(projectMetadata.versionRevision, isNull); - expect(logger.traceText, contains('.metadata project_type version is malformed.')); - expect(logger.traceText, contains('.metadata version is malformed.')); + expect(logger.traceText, contains('.metadata file at .metadata was empty or malformed.')); }); testWithoutContext('project metadata fields are empty when file is not valid yaml', () { @@ -48,8 +47,7 @@ void main() { expect(projectMetadata.versionChannel, isNull); expect(projectMetadata.versionRevision, isNull); - expect(logger.traceText, contains('.metadata project_type version is malformed.')); - expect(logger.traceText, contains('.metadata version is malformed.')); + expect(logger.traceText, contains('.metadata file at .metadata was empty or malformed.')); }); testWithoutContext('projectType is populated when version is malformed', () { @@ -64,7 +62,7 @@ project_type: plugin expect(projectMetadata.versionChannel, isNull); expect(projectMetadata.versionRevision, isNull); - expect(logger.traceText, contains('.metadata version is malformed.')); + expect(logger.traceText, contains('The value of key `version` in .metadata was expected to be YamlMap but was String')); }); testWithoutContext('version is populated when projectType is malformed', () { @@ -81,6 +79,94 @@ project_type: {} expect(projectMetadata.versionChannel, 'stable'); expect(projectMetadata.versionRevision, 'b59b226a49391949247e3d6122e34bb001049ae4'); - expect(logger.traceText, contains('.metadata project_type version is malformed.')); + expect(logger.traceText, contains('The value of key `project_type` in .metadata was expected to be String but was YamlMap')); + }); + + testWithoutContext('migrate config is populated when version is malformed', () { + metadataFile + ..createSync() + ..writeAsStringSync(''' +version: STRING INSTEAD OF MAP +project_type: {} + +migration: + platforms: + - platform: root + create_revision: abcdefg + base_revision: baserevision + + unmanaged_files: + - 'file1' + '''); + final FlutterProjectMetadata projectMetadata = FlutterProjectMetadata(metadataFile, logger); + expect(projectMetadata.projectType, isNull); + expect(projectMetadata.migrateConfig.platformConfigs[SupportedPlatform.root]?.createRevision, 'abcdefg'); + expect(projectMetadata.migrateConfig.platformConfigs[SupportedPlatform.root]?.baseRevision, 'baserevision'); + expect(projectMetadata.migrateConfig.unmanagedFiles[0], 'file1'); + + expect(logger.traceText, contains('The value of key `version` in .metadata was expected to be YamlMap but was String')); + expect(logger.traceText, contains('The value of key `project_type` in .metadata was expected to be String but was YamlMap')); + }); + + testWithoutContext('migrate config is populated when unmanaged_files is malformed', () { + metadataFile + ..createSync() + ..writeAsStringSync(''' +version: + revision: b59b226a49391949247e3d6122e34bb001049ae4 + channel: stable +project_type: app + +migration: + platforms: + - platform: root + create_revision: abcdefg + base_revision: baserevision + + unmanaged_files: {} + '''); + final FlutterProjectMetadata projectMetadata = FlutterProjectMetadata(metadataFile, logger); + expect(projectMetadata.projectType, FlutterProjectType.app); + expect(projectMetadata.migrateConfig.platformConfigs[SupportedPlatform.root]?.createRevision, 'abcdefg'); + expect(projectMetadata.migrateConfig.platformConfigs[SupportedPlatform.root]?.baseRevision, 'baserevision'); + // Tool uses default unamanged files list when malformed. + expect(projectMetadata.migrateConfig.unmanagedFiles[0], 'lib/main.dart'); + + expect(logger.traceText, contains('The value of key `unmanaged_files` in .metadata was expected to be YamlList but was YamlMap')); + }); + + testWithoutContext('platforms is populated with a malformed entry', () { + metadataFile + ..createSync() + ..writeAsStringSync(''' +version: + revision: b59b226a49391949247e3d6122e34bb001049ae4 + channel: stable +project_type: app + +migration: + platforms: + - platform: root + create_revision: abcdefg + base_revision: baserevision + - platform: android + base_revision: baserevision + - platform: ios + create_revision: abcdefg + base_revision: baserevision + + unmanaged_files: + - 'file1' + '''); + final FlutterProjectMetadata projectMetadata = FlutterProjectMetadata(metadataFile, logger); + expect(projectMetadata.projectType, FlutterProjectType.app); + expect(projectMetadata.migrateConfig.platformConfigs[SupportedPlatform.root]?.createRevision, 'abcdefg'); + expect(projectMetadata.migrateConfig.platformConfigs[SupportedPlatform.root]?.baseRevision, 'baserevision'); + expect(projectMetadata.migrateConfig.platformConfigs[SupportedPlatform.ios]?.createRevision, 'abcdefg'); + expect(projectMetadata.migrateConfig.platformConfigs[SupportedPlatform.ios]?.baseRevision, 'baserevision'); + expect(projectMetadata.migrateConfig.platformConfigs.containsKey(SupportedPlatform.android), false); + expect(projectMetadata.migrateConfig.unmanagedFiles[0], 'file1'); + + expect(logger.traceText, contains('The key `create_revision` was not found')); }); } diff --git a/packages/flutter_tools/test/integration.shard/migrate_config_test.dart b/packages/flutter_tools/test/integration.shard/migrate_config_test.dart new file mode 100644 index 0000000000..9973356dfb --- /dev/null +++ b/packages/flutter_tools/test/integration.shard/migrate_config_test.dart @@ -0,0 +1,231 @@ +// 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. + +// @dart = 2.8 + +import 'package:file/file.dart'; +import 'package:flutter_tools/src/base/logger.dart'; +import 'package:flutter_tools/src/flutter_project_metadata.dart'; +import 'package:flutter_tools/src/project.dart'; + +import '../src/common.dart'; +import '../src/context.dart'; +import 'test_data/migrate_project.dart'; +import 'test_driver.dart'; +import 'test_utils.dart'; + + +void main() { + Directory tempDir; + FlutterRunTestDriver flutter; + Logger logger; + + setUp(() async { + tempDir = createResolvedTempDirectorySync('run_test.'); + flutter = FlutterRunTestDriver(tempDir); + logger = BufferLogger.test(); + }); + + tearDown(() async { + await flutter.stop(); + tryToDelete(tempDir); + }); + + testWithoutContext('parse simple config file', () async { + final File metadataFile = tempDir.childFile('.metadata'); + metadataFile.createSync(recursive: true); + metadataFile.writeAsStringSync(''' +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled. + +version: + revision: fj19vkla9vnlka9vni3n808v3nch8cd + channel: stable + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: fj19vkla9vnlka9vni3n808v3nch8cd + base_revision: 93kf9v3njfa90vnidfjvn39nvi3vnie + - platform: android + create_revision: abfj19vkla9vnlka9vni3n808v3nch8cd + base_revision: ab93kf9v3njfa90vnidfjvn39nvi3vnie + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - lib/main.dart + - ios/Runner.xcodeproj/project.pbxproj + - lib/file1/etc.dart + - android/my_file.java +''', flush: true); + FlutterProjectMetadata metadata = FlutterProjectMetadata(metadataFile, logger); + + expect(metadata.migrateConfig.platformConfigs[SupportedPlatform.root].createRevision, equals('fj19vkla9vnlka9vni3n808v3nch8cd')); + expect(metadata.migrateConfig.platformConfigs[SupportedPlatform.root].baseRevision, equals('93kf9v3njfa90vnidfjvn39nvi3vnie')); + + expect(metadata.migrateConfig.platformConfigs[SupportedPlatform.android].createRevision, equals('abfj19vkla9vnlka9vni3n808v3nch8cd')); + expect(metadata.migrateConfig.platformConfigs[SupportedPlatform.android].baseRevision, equals('ab93kf9v3njfa90vnidfjvn39nvi3vnie')); + + expect(metadata.migrateConfig.unmanagedFiles[0], equals('lib/main.dart')); + expect(metadata.migrateConfig.unmanagedFiles[1], equals('ios/Runner.xcodeproj/project.pbxproj')); + expect(metadata.migrateConfig.unmanagedFiles[2], equals('lib/file1/etc.dart')); + expect(metadata.migrateConfig.unmanagedFiles[3], equals('android/my_file.java')); + + metadataFile.writeAsStringSync(''' +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled. + +version: + revision: fj19vkla9vnlka9vni3n808v3nch8cd + channel: stable + +project_type: app +''', flush: true); + + metadata = FlutterProjectMetadata(metadataFile, logger); + + expect(metadata.migrateConfig.isEmpty, equals(true)); + expect(metadata.versionRevision, equals('fj19vkla9vnlka9vni3n808v3nch8cd')); + expect(metadata.versionChannel, equals('stable')); + }); + + testUsingContext('write simple config file', () async { + const String testCreateRevision = 'testmc9skl32nlnf23lnakcs9njr3'; + const String testBaseRevision = 'testanas9anlnq9ba7bjhavan3kma'; + MigrateConfig config = MigrateConfig( + platformConfigs: { + SupportedPlatform.android: MigratePlatformConfig(createRevision: testCreateRevision, baseRevision: testBaseRevision), + SupportedPlatform.ios: MigratePlatformConfig(createRevision: testCreateRevision, baseRevision: testBaseRevision), + SupportedPlatform.root: MigratePlatformConfig(createRevision: testCreateRevision, baseRevision: testBaseRevision), + SupportedPlatform.windows: MigratePlatformConfig(createRevision: testCreateRevision, baseRevision: testBaseRevision), + }, + unmanagedFiles: [ + 'lib/main.dart', + 'ios/Runner.xcodeproj/project.pbxproj', + 'lib/file1/etc.dart', + ], + ); + String outputString = config.getOutputFileString(); + expect(outputString, equals(''' + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: android + create_revision: $testCreateRevision + base_revision: $testBaseRevision + - platform: ios + create_revision: $testCreateRevision + base_revision: $testBaseRevision + - platform: root + create_revision: $testCreateRevision + base_revision: $testBaseRevision + - platform: windows + create_revision: $testCreateRevision + base_revision: $testBaseRevision + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' + - 'lib/file1/etc.dart' +''')); + + config = MigrateConfig(); + outputString = config.getOutputFileString(); + expect(outputString, equals('')); + }); + + testUsingContext('populate migrate config', () async { + // Flutter Stable 1.22.6 hash: 9b2d32b605630f28625709ebd9d78ab3016b2bf6 + final MigrateProject project = MigrateProject('version:1.22.6_stable'); + await project.setUpIn(tempDir); + + final File metadataFile = tempDir.childFile('.metadata'); + + const String currentRevision = 'test_base_revision'; + const String createRevision = 'test_create_revision'; + + final FlutterProjectMetadata metadata = FlutterProjectMetadata(metadataFile, logger); + metadata.migrateConfig.populate( + projectDirectory: tempDir, + currentRevision: currentRevision, + createRevision: createRevision, + create: true, + update: true, + logger: logger, + ); + + expect(metadata.migrateConfig.platformConfigs.length, equals(3)); + + final List keyList = List.from(metadata.migrateConfig.platformConfigs.keys); + + expect(keyList[0], equals(SupportedPlatform.root)); + expect(metadata.migrateConfig.platformConfigs[SupportedPlatform.root].baseRevision, equals(currentRevision)); + expect(metadata.migrateConfig.platformConfigs[SupportedPlatform.root].createRevision, equals(createRevision)); + + expect(keyList[1], equals(SupportedPlatform.android)); + expect(metadata.migrateConfig.platformConfigs[SupportedPlatform.android].baseRevision, equals(currentRevision)); + expect(metadata.migrateConfig.platformConfigs[SupportedPlatform.android].createRevision, equals(createRevision)); + + expect(keyList[2], equals(SupportedPlatform.ios)); + expect(metadata.migrateConfig.platformConfigs[SupportedPlatform.ios].baseRevision, equals(currentRevision)); + expect(metadata.migrateConfig.platformConfigs[SupportedPlatform.ios].createRevision, equals(createRevision)); + + final File metadataFileOutput = tempDir.childFile('.metadata_output'); + metadata.writeFile(outputFile: metadataFileOutput); + expect(metadataFileOutput.readAsStringSync(), equals(''' +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled. + +version: + revision: 9b2d32b605630f28625709ebd9d78ab3016b2bf6 + channel: unknown + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: $createRevision + base_revision: $currentRevision + - platform: android + create_revision: $createRevision + base_revision: $currentRevision + - platform: ios + create_revision: $createRevision + base_revision: $currentRevision + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' +''')); + }); +} diff --git a/packages/flutter_tools/test/integration.shard/test_data/full_apps/vanilla_app_1_22_6_stable.ensure b/packages/flutter_tools/test/integration.shard/test_data/full_apps/vanilla_app_1_22_6_stable.ensure new file mode 100644 index 0000000000..b6d130f8a8 --- /dev/null +++ b/packages/flutter_tools/test/integration.shard/test_data/full_apps/vanilla_app_1_22_6_stable.ensure @@ -0,0 +1 @@ +flutter/test/full_app_fixtures/vanilla version:1.22.6_stable diff --git a/packages/flutter_tools/test/integration.shard/test_data/migrate_project.dart b/packages/flutter_tools/test/integration.shard/test_data/migrate_project.dart new file mode 100644 index 0000000000..e14d8b1597 --- /dev/null +++ b/packages/flutter_tools/test/integration.shard/test_data/migrate_project.dart @@ -0,0 +1,237 @@ +// 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. + +@Timeout(Duration(seconds: 600)) + +import 'dart:io'; +import 'package:file/file.dart'; + +import '../../src/common.dart'; +import '../test_utils.dart'; +import 'project.dart'; + +class MigrateProject extends Project { + MigrateProject(this.version, {this.vanilla = true}); + + @override + Future setUpIn(Directory dir, { + bool useDeferredLoading = false, + bool useSyntheticPackage = false, + }) async { + this.dir = dir; + _appPath = dir.path; + if (androidLocalProperties != null) { + writeFile(fileSystem.path.join(dir.path, 'android', 'local.properties'), androidLocalProperties); + } + final Directory tempDir = createResolvedTempDirectorySync('cipd_dest.'); + final Directory depotToolsDir = createResolvedTempDirectorySync('depot_tools.'); + + await processManager.run([ + 'git', + 'clone', + 'https://chromium.googlesource.com/chromium/tools/depot_tools', + depotToolsDir.path, + ], workingDirectory: dir.path); + + final File cipdFile = depotToolsDir.childFile(Platform.isWindows ? 'cipd.bat' : 'cipd'); + await processManager.run([ + cipdFile.path, + 'init', + tempDir.path, + '-force', + ], workingDirectory: dir.path); + + await processManager.run([ + cipdFile.path, + 'install', + 'flutter/test/full_app_fixtures/vanilla', + version, + '-root', + tempDir.path, + ], workingDirectory: dir.path); + + if (Platform.isWindows) { + await processManager.run([ + 'robocopy', + tempDir.path, + dir.path, + '*', + '/E', + '/mov', + ]); + // Add full access permissions to Users + await processManager.run([ + 'icacls', + tempDir.path, + '/q', + '/c', + '/t', + '/grant', + 'Users:F', + ]); + } else { + // This cp command changes the symlinks to real files so the tool can edit them. + await processManager.run([ + 'cp', + '-R', + '-L', + '-f', + '${tempDir.path}/.', + dir.path, + ]); + + await processManager.run([ + 'rm', + '-rf', + '.cipd', + ], workingDirectory: dir.path); + + final List allFiles = dir.listSync(recursive: true); + for (final FileSystemEntity file in allFiles) { + if (file is! File) { + continue; + } + await processManager.run([ + 'chmod', + '+w', + file.path, + ], workingDirectory: dir.path); + } + } + + if (!vanilla) { + writeFile(fileSystem.path.join(dir.path, 'lib', 'main.dart'), libMain); + writeFile(fileSystem.path.join(dir.path, 'lib', 'other.dart'), libOther); + writeFile(fileSystem.path.join(dir.path, 'pubspec.yaml'), pubspecCustom); + } + tryToDelete(tempDir); + tryToDelete(depotToolsDir); + } + + final String version; + final bool vanilla; + late String _appPath; + + // Maintain the same pubspec as the configured app. + @override + String get pubspec => fileSystem.file(fileSystem.path.join(_appPath, 'pubspec.yaml')).readAsStringSync(); + + String get androidLocalProperties => ''' + flutter.sdk=${getFlutterRoot()} + '''; + + String get libMain => ''' +import 'package:flutter/material.dart'; +import 'other.dart'; + +void main() { + runApp(MyApp()); +} + +class MyApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Flutter Demo', + theme: ThemeData( + primarySwatch: Colors.blue, + visualDensity: VisualDensity.adaptivePlatformDensity, + ), + home: OtherWidget(), + ); + } +} + +'''; + + String get libOther => ''' +class OtherWidget extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Container(width: 100, height: 100); + } +} + +'''; + + String get pubspecCustom => ''' +name: vanilla_app_1_22_6_stable +description: This is a modified description from the default. + +# The following line prevents the package from being accidentally published to +# pub.dev using `pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +version: 1.0.0+1 + +environment: + sdk: ">=2.6.0 <3.0.0" + +dependencies: + flutter: + sdk: flutter + + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.0 + +dev_dependencies: + flutter_test: + sdk: flutter + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + assets: + - images/a_dot_burr.jpeg + - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware. + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/assets-and-images/#from-packages + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/custom-fonts/#from-packages + +'''; +} diff --git a/packages/flutter_tools/test/integration.shard/test_utils.dart b/packages/flutter_tools/test/integration.shard/test_utils.dart index fe2af16138..94bb3f4f2a 100644 --- a/packages/flutter_tools/test/integration.shard/test_utils.dart +++ b/packages/flutter_tools/test/integration.shard/test_utils.dart @@ -33,7 +33,7 @@ Directory createResolvedTempDirectorySync(String prefix) { void writeFile(String path, String content, {bool writeFutureModifiedDate = false}) { final File file = fileSystem.file(path) ..createSync(recursive: true) - ..writeAsStringSync(content); + ..writeAsStringSync(content, flush: true); // Some integration tests on Windows to not see this file as being modified // recently enough for the hot reload to pick this change up unless the // modified time is written in the future.