MigrateConfig and migrate integration testing base (#99092)
This commit is contained in:
parent
1c2c942112
commit
63ff7a199b
@ -320,6 +320,7 @@ class CreateCommand extends CreateBase {
|
|||||||
templateContext,
|
templateContext,
|
||||||
overwrite: overwrite,
|
overwrite: overwrite,
|
||||||
printStatusWhenWriting: !creatingNewProject,
|
printStatusWhenWriting: !creatingNewProject,
|
||||||
|
projectType: template,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case FlutterProjectType.skeleton:
|
case FlutterProjectType.skeleton:
|
||||||
@ -329,6 +330,7 @@ class CreateCommand extends CreateBase {
|
|||||||
templateContext,
|
templateContext,
|
||||||
overwrite: overwrite,
|
overwrite: overwrite,
|
||||||
printStatusWhenWriting: !creatingNewProject,
|
printStatusWhenWriting: !creatingNewProject,
|
||||||
|
generateMetadata: false,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case FlutterProjectType.module:
|
case FlutterProjectType.module:
|
||||||
@ -353,6 +355,7 @@ class CreateCommand extends CreateBase {
|
|||||||
templateContext,
|
templateContext,
|
||||||
overwrite: overwrite,
|
overwrite: overwrite,
|
||||||
printStatusWhenWriting: !creatingNewProject,
|
printStatusWhenWriting: !creatingNewProject,
|
||||||
|
projectType: template,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case FlutterProjectType.ffiPlugin:
|
case FlutterProjectType.ffiPlugin:
|
||||||
@ -361,6 +364,7 @@ class CreateCommand extends CreateBase {
|
|||||||
templateContext,
|
templateContext,
|
||||||
overwrite: overwrite,
|
overwrite: overwrite,
|
||||||
printStatusWhenWriting: !creatingNewProject,
|
printStatusWhenWriting: !creatingNewProject,
|
||||||
|
projectType: template,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -495,6 +499,7 @@ Your $application code is in $relativeAppMain.
|
|||||||
Map<String, dynamic> templateContext, {
|
Map<String, dynamic> templateContext, {
|
||||||
bool overwrite = false,
|
bool overwrite = false,
|
||||||
bool printStatusWhenWriting = true,
|
bool printStatusWhenWriting = true,
|
||||||
|
FlutterProjectType projectType,
|
||||||
}) async {
|
}) async {
|
||||||
// Plugins only add a platform if it was requested explicitly by the user.
|
// Plugins only add a platform if it was requested explicitly by the user.
|
||||||
if (!argResults.wasParsed('platforms')) {
|
if (!argResults.wasParsed('platforms')) {
|
||||||
@ -561,6 +566,7 @@ Your $application code is in $relativeAppMain.
|
|||||||
overwrite: overwrite,
|
overwrite: overwrite,
|
||||||
pluginExampleApp: true,
|
pluginExampleApp: true,
|
||||||
printStatusWhenWriting: printStatusWhenWriting,
|
printStatusWhenWriting: printStatusWhenWriting,
|
||||||
|
projectType: projectType,
|
||||||
);
|
);
|
||||||
return generatedCount;
|
return generatedCount;
|
||||||
}
|
}
|
||||||
@ -570,6 +576,7 @@ Your $application code is in $relativeAppMain.
|
|||||||
Map<String, dynamic> templateContext, {
|
Map<String, dynamic> templateContext, {
|
||||||
bool overwrite = false,
|
bool overwrite = false,
|
||||||
bool printStatusWhenWriting = true,
|
bool printStatusWhenWriting = true,
|
||||||
|
FlutterProjectType projectType,
|
||||||
}) async {
|
}) async {
|
||||||
// Plugins only add a platform if it was requested explicitly by the user.
|
// Plugins only add a platform if it was requested explicitly by the user.
|
||||||
if (!argResults.wasParsed('platforms')) {
|
if (!argResults.wasParsed('platforms')) {
|
||||||
@ -637,6 +644,7 @@ Your $application code is in $relativeAppMain.
|
|||||||
overwrite: overwrite,
|
overwrite: overwrite,
|
||||||
pluginExampleApp: true,
|
pluginExampleApp: true,
|
||||||
printStatusWhenWriting: printStatusWhenWriting,
|
printStatusWhenWriting: printStatusWhenWriting,
|
||||||
|
projectType: projectType,
|
||||||
);
|
);
|
||||||
return generatedCount;
|
return generatedCount;
|
||||||
}
|
}
|
||||||
|
@ -134,6 +134,13 @@ abstract class CreateBase extends FlutterCommand {
|
|||||||
'This is only intended to enable testing of the tool itself.',
|
'This is only intended to enable testing of the tool itself.',
|
||||||
hide: !verboseHelp,
|
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.
|
/// The output directory of the command.
|
||||||
@ -488,6 +495,8 @@ abstract class CreateBase extends FlutterCommand {
|
|||||||
bool overwrite = false,
|
bool overwrite = false,
|
||||||
bool pluginExampleApp = false,
|
bool pluginExampleApp = false,
|
||||||
bool printStatusWhenWriting = true,
|
bool printStatusWhenWriting = true,
|
||||||
|
bool generateMetadata = true,
|
||||||
|
FlutterProjectType projectType,
|
||||||
}) async {
|
}) async {
|
||||||
int generatedCount = 0;
|
int generatedCount = 0;
|
||||||
generatedCount += await renderMerged(
|
generatedCount += await renderMerged(
|
||||||
@ -502,6 +511,14 @@ abstract class CreateBase extends FlutterCommand {
|
|||||||
generatedCount += _injectGradleWrapper(project);
|
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')) {
|
if (boolArg('pub')) {
|
||||||
final Environment environment = Environment(
|
final Environment environment = Environment(
|
||||||
artifacts: globals.artifacts,
|
artifacts: globals.artifacts,
|
||||||
@ -534,18 +551,63 @@ abstract class CreateBase extends FlutterCommand {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await project.ensureReadyForPlatformSpecificTooling(
|
await project.ensureReadyForPlatformSpecificTooling(
|
||||||
androidPlatform: templateContext['android'] as bool ?? false,
|
androidPlatform: androidPlatform,
|
||||||
iosPlatform: templateContext['ios'] as bool ?? false,
|
iosPlatform: iosPlatform,
|
||||||
linuxPlatform: templateContext['linux'] as bool ?? false,
|
linuxPlatform: linuxPlatform,
|
||||||
macOSPlatform: templateContext['macos'] as bool ?? false,
|
macOSPlatform: macOSPlatform,
|
||||||
windowsPlatform: templateContext['windows'] as bool ?? false,
|
windowsPlatform: windowsPlatform,
|
||||||
webPlatform: templateContext['web'] as bool ?? false,
|
webPlatform: webPlatform,
|
||||||
winUwpPlatform: templateContext['winuwp'] as bool ?? false,
|
winUwpPlatform: winUwpPlatform,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (templateContext['android'] == true) {
|
final List<SupportedPlatform> platformsForMigrateConfig = <SupportedPlatform>[SupportedPlatform.root];
|
||||||
|
if (androidPlatform) {
|
||||||
gradle.updateLocalProperties(project: project, requireAndroidSdk: false);
|
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;
|
return generatedCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,6 +7,8 @@ import 'package:yaml/yaml.dart';
|
|||||||
import 'base/file_system.dart';
|
import 'base/file_system.dart';
|
||||||
import 'base/logger.dart';
|
import 'base/logger.dart';
|
||||||
import 'base/utils.dart';
|
import 'base/utils.dart';
|
||||||
|
import 'project.dart';
|
||||||
|
import 'version.dart';
|
||||||
|
|
||||||
enum FlutterProjectType {
|
enum FlutterProjectType {
|
||||||
/// This is the default project with the user-managed host code.
|
/// This is the default project with the user-managed host code.
|
||||||
@ -27,7 +29,10 @@ enum FlutterProjectType {
|
|||||||
ffiPlugin,
|
ffiPlugin,
|
||||||
}
|
}
|
||||||
|
|
||||||
String flutterProjectTypeToString(FlutterProjectType type) {
|
String flutterProjectTypeToString(FlutterProjectType? type) {
|
||||||
|
if (type == null) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
if (type == FlutterProjectType.ffiPlugin) {
|
if (type == FlutterProjectType.ffiPlugin) {
|
||||||
return 'plugin_ffi';
|
return 'plugin_ffi';
|
||||||
}
|
}
|
||||||
@ -45,67 +50,286 @@ FlutterProjectType? stringToProjectType(String value) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Verifies the expected yaml keys are present in the file.
|
||||||
|
bool _validateMetadataMap(Object? yamlRoot, Map<String, Type> validations, Logger logger) {
|
||||||
|
if (yamlRoot != null && yamlRoot is! YamlMap) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final YamlMap map = yamlRoot! as YamlMap;
|
||||||
|
bool isValid = true;
|
||||||
|
for (final MapEntry<String, Object> 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.
|
/// A wrapper around the `.metadata` file.
|
||||||
class FlutterProjectMetadata {
|
class FlutterProjectMetadata {
|
||||||
FlutterProjectMetadata(
|
/// Creates a MigrateConfig by parsing an existing .migrate_config yaml file.
|
||||||
File metadataFile,
|
FlutterProjectMetadata(File file, Logger logger) : _metadataFile = file,
|
||||||
Logger logger,
|
_logger = logger,
|
||||||
) : _metadataFile = metadataFile,
|
migrateConfig = MigrateConfig() {
|
||||||
_logger = logger;
|
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, <String, Type>{'version': YamlMap}, _logger)) {
|
||||||
|
final Object? versionYaml = map['version'];
|
||||||
|
if (_validateMetadataMap(versionYaml, <String, Type>{
|
||||||
|
'revision': String,
|
||||||
|
'channel': String,
|
||||||
|
}, _logger)) {
|
||||||
|
final YamlMap versionYamlMap = versionYaml! as YamlMap;
|
||||||
|
_versionRevision = versionYamlMap['revision'] as String?;
|
||||||
|
_versionChannel = versionYamlMap['channel'] as String?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (_validateMetadataMap(yamlRoot, <String, Type>{'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;
|
final Logger _logger;
|
||||||
|
|
||||||
String? get versionChannel => _versionValue('channel');
|
final File _metadataFile;
|
||||||
String? get versionRevision => _versionValue('revision');
|
|
||||||
|
|
||||||
FlutterProjectType? get projectType {
|
/// Writes the .migrate_config file in the provided project directory's platform subdirectory.
|
||||||
final dynamic projectTypeYaml = _metadataValue('project_type');
|
///
|
||||||
if (projectTypeYaml is String) {
|
/// We write the file manually instead of with a template because this
|
||||||
return stringToProjectType(projectTypeYaml);
|
/// needs to be able to write the .migrate_config file into legacy apps.
|
||||||
} else {
|
void writeFile({File? outputFile}) {
|
||||||
_logger.printTrace('.metadata project_type version is malformed.');
|
outputFile = outputFile ?? _metadataFile;
|
||||||
return null;
|
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;
|
void populate({
|
||||||
String? _versionValue(String key) {
|
List<SupportedPlatform>? platforms,
|
||||||
if (_versionYaml == null) {
|
Directory? projectDirectory,
|
||||||
final dynamic versionYaml = _metadataValue('version');
|
String? currentRevision,
|
||||||
if (versionYaml is YamlMap) {
|
String? createRevision,
|
||||||
_versionYaml = versionYaml;
|
bool create = true,
|
||||||
} else {
|
bool update = true,
|
||||||
_logger.printTrace('.metadata version is malformed.');
|
required Logger logger,
|
||||||
return null;
|
}) {
|
||||||
}
|
migrateConfig.populate(
|
||||||
}
|
platforms: platforms,
|
||||||
if (_versionYaml != null && _versionYaml!.containsKey(key) && _versionYaml![key] is String) {
|
projectDirectory: projectDirectory,
|
||||||
return _versionYaml![key] as String;
|
currentRevision: currentRevision,
|
||||||
}
|
createRevision: createRevision,
|
||||||
return null;
|
create: create,
|
||||||
|
update: update,
|
||||||
|
logger: logger,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
YamlMap? _metadataYaml;
|
/// Finds the fallback revision to use when no base revision is found in the migrate config.
|
||||||
dynamic _metadataValue(String key) {
|
String getFallbackBaseRevision(Logger logger, FlutterVersion flutterVersion) {
|
||||||
if (_metadataYaml == null) {
|
// Use the .metadata file if it exists.
|
||||||
if (!_metadataFile.existsSync()) {
|
if (versionRevision != null) {
|
||||||
return null;
|
return versionRevision!;
|
||||||
}
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return flutterVersion.frameworkRevision;
|
||||||
return _metadataYaml![key];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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<SupportedPlatform, MigratePlatformConfig>? platformConfigs,
|
||||||
|
this.unmanagedFiles = _kDefaultUnmanagedFiles
|
||||||
|
}) : platformConfigs = platformConfigs ?? <SupportedPlatform, MigratePlatformConfig>{};
|
||||||
|
|
||||||
|
/// A mapping of the files that are unmanaged by defult for each platform.
|
||||||
|
static const List<String> _kDefaultUnmanagedFiles = <String>[
|
||||||
|
'lib/main.dart',
|
||||||
|
'ios/Runner.xcodeproj/project.pbxproj',
|
||||||
|
];
|
||||||
|
|
||||||
|
/// The metadata for each platform supported by the project.
|
||||||
|
final Map<SupportedPlatform, MigratePlatformConfig> 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<String> 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<SupportedPlatform>? 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<SupportedPlatform, MigratePlatformConfig> 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, <String, Type>{'platforms': YamlList}, logger)) {
|
||||||
|
if (platformsYaml is YamlList && platformsYaml.isNotEmpty) {
|
||||||
|
for (final Object? platform in platformsYaml) {
|
||||||
|
if (_validateMetadataMap(platform, <String, Type>{
|
||||||
|
'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, <String, Type>{'unmanaged_files': YamlList}, logger)) {
|
||||||
|
final Object? unmanagedFilesYaml = map['unmanaged_files'];
|
||||||
|
if (unmanagedFilesYaml is YamlList && unmanagedFilesYaml.isNotEmpty) {
|
||||||
|
unmanagedFiles = List<String>.from(unmanagedFilesYaml.value.cast<String>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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;
|
||||||
|
}
|
||||||
|
@ -27,6 +27,19 @@ import 'xcode_project.dart';
|
|||||||
export 'cmake_project.dart';
|
export 'cmake_project.dart';
|
||||||
export 'xcode_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 {
|
class FlutterProjectFactory {
|
||||||
FlutterProjectFactory({
|
FlutterProjectFactory({
|
||||||
required Logger logger,
|
required Logger logger,
|
||||||
@ -244,6 +257,36 @@ class FlutterProject {
|
|||||||
/// True if this project has an example application.
|
/// True if this project has an example application.
|
||||||
bool get hasExampleApp => _exampleDirectory(directory).existsSync();
|
bool get hasExampleApp => _exampleDirectory(directory).existsSync();
|
||||||
|
|
||||||
|
/// Returns a list of platform names that are supported by the project.
|
||||||
|
List<SupportedPlatform> getSupportedPlatforms({bool includeRoot = false}) {
|
||||||
|
final List<SupportedPlatform> platforms = includeRoot ? <SupportedPlatform>[SupportedPlatform.root] : <SupportedPlatform>[];
|
||||||
|
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.
|
/// The directory that will contain the example if an example exists.
|
||||||
static Directory _exampleDirectory(Directory directory) => directory.childDirectory('example');
|
static Directory _exampleDirectory(Directory directory) => directory.childDirectory('example');
|
||||||
|
|
||||||
@ -555,12 +598,10 @@ class AndroidProject extends FlutterProjectPlatform {
|
|||||||
if (deprecationBehavior == DeprecationBehavior.none) {
|
if (deprecationBehavior == DeprecationBehavior.none) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final AndroidEmbeddingVersionResult result = computeEmbeddingVersion();
|
final AndroidEmbeddingVersionResult result = computeEmbeddingVersion();
|
||||||
if (result.version != AndroidEmbeddingVersion.v1) {
|
if (result.version != AndroidEmbeddingVersion.v1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
globals.printStatus(
|
globals.printStatus(
|
||||||
'''
|
'''
|
||||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
@ -584,11 +625,11 @@ The detected reason was:
|
|||||||
if (deprecationBehavior == DeprecationBehavior.ignore) {
|
if (deprecationBehavior == DeprecationBehavior.ignore) {
|
||||||
BuildEvent('deprecated-v1-android-embedding-ignored', type: 'gradle', flutterUsage: globals.flutterUsage).send();
|
BuildEvent('deprecated-v1-android-embedding-ignored', type: 'gradle', flutterUsage: globals.flutterUsage).send();
|
||||||
} else { // DeprecationBehavior.exit
|
} else { // DeprecationBehavior.exit
|
||||||
BuildEvent('deprecated-v1-android-embedding-failed', type: 'gradle', flutterUsage: globals.flutterUsage).send();
|
BuildEvent('deprecated-v1-android-embedding-failed', type: 'gradle', flutterUsage: globals.flutterUsage).send();
|
||||||
throwToolExit(
|
throwToolExit(
|
||||||
'Build failed due to use of deprecated Android v1 embedding.',
|
'Build failed due to use of deprecated Android v1 embedding.',
|
||||||
exitCode: 1,
|
exitCode: 1,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -134,6 +134,12 @@ class IosProject extends XcodeBasedProject {
|
|||||||
|
|
||||||
Directory get symlinks => _flutterLibRoot.childDirectory('.symlinks');
|
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?
|
/// Do all plugins support arm64 simulators to run natively on an ARM Mac?
|
||||||
Future<bool> pluginsSupportArmSimulator() async {
|
Future<bool> pluginsSupportArmSimulator() async {
|
||||||
final Directory podXcodeProject = hostAppRoot
|
final Directory podXcodeProject = hostAppRoot
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
.buildlog/
|
.buildlog/
|
||||||
.history
|
.history
|
||||||
.svn/
|
.svn/
|
||||||
|
migrate_working_dir/
|
||||||
|
|
||||||
# IntelliJ related
|
# IntelliJ related
|
||||||
*.iml
|
*.iml
|
||||||
|
@ -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
|
|
@ -9,6 +9,8 @@
|
|||||||
.sconsign.dblite
|
.sconsign.dblite
|
||||||
.svn/
|
.svn/
|
||||||
|
|
||||||
|
migrate_working_dir/
|
||||||
|
|
||||||
*.swp
|
*.swp
|
||||||
profile
|
profile
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
.buildlog/
|
.buildlog/
|
||||||
.history
|
.history
|
||||||
.svn/
|
.svn/
|
||||||
|
migrate_working_dir/
|
||||||
|
|
||||||
# IntelliJ related
|
# IntelliJ related
|
||||||
*.iml
|
*.iml
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
.buildlog/
|
.buildlog/
|
||||||
.history
|
.history
|
||||||
.svn/
|
.svn/
|
||||||
|
migrate_working_dir/
|
||||||
|
|
||||||
# IntelliJ related
|
# IntelliJ related
|
||||||
*.iml
|
*.iml
|
||||||
|
@ -88,19 +88,19 @@ void main() {
|
|||||||
await runner.run(<String>['create', '--no-pub', '--template=module', 'testy']);
|
await runner.run(<String>['create', '--no-pub', '--template=module', 'testy']);
|
||||||
expect((await command.usageValues).commandCreateProjectType, 'module');
|
expect((await command.usageValues).commandCreateProjectType, 'module');
|
||||||
|
|
||||||
await runner.run(<String>['create', '--no-pub', '--template=app', 'testy']);
|
await runner.run(<String>['create', '--no-pub', '--template=app', 'testy1']);
|
||||||
expect((await command.usageValues).commandCreateProjectType, 'app');
|
expect((await command.usageValues).commandCreateProjectType, 'app');
|
||||||
|
|
||||||
await runner.run(<String>['create', '--no-pub', '--template=skeleton', 'testy']);
|
await runner.run(<String>['create', '--no-pub', '--template=skeleton', 'testy2']);
|
||||||
expect((await command.usageValues).commandCreateProjectType, 'skeleton');
|
expect((await command.usageValues).commandCreateProjectType, 'skeleton');
|
||||||
|
|
||||||
await runner.run(<String>['create', '--no-pub', '--template=package', 'testy']);
|
await runner.run(<String>['create', '--no-pub', '--template=package', 'testy3']);
|
||||||
expect((await command.usageValues).commandCreateProjectType, 'package');
|
expect((await command.usageValues).commandCreateProjectType, 'package');
|
||||||
|
|
||||||
await runner.run(<String>['create', '--no-pub', '--template=plugin', 'testy']);
|
await runner.run(<String>['create', '--no-pub', '--template=plugin', 'testy4']);
|
||||||
expect((await command.usageValues).commandCreateProjectType, 'plugin');
|
expect((await command.usageValues).commandCreateProjectType, 'plugin');
|
||||||
|
|
||||||
await runner.run(<String>['create', '--no-pub', '--template=plugin_ffi', 'testy']);
|
await runner.run(<String>['create', '--no-pub', '--template=plugin_ffi', 'testy5']);
|
||||||
expect((await command.usageValues).commandCreateProjectType, 'plugin_ffi');
|
expect((await command.usageValues).commandCreateProjectType, 'plugin_ffi');
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ import 'package:file/memory.dart';
|
|||||||
import 'package:flutter_tools/src/base/file_system.dart';
|
import 'package:flutter_tools/src/base/file_system.dart';
|
||||||
import 'package:flutter_tools/src/base/logger.dart';
|
import 'package:flutter_tools/src/base/logger.dart';
|
||||||
import 'package:flutter_tools/src/flutter_project_metadata.dart';
|
import 'package:flutter_tools/src/flutter_project_metadata.dart';
|
||||||
|
import 'package:flutter_tools/src/project.dart';
|
||||||
|
|
||||||
import '../src/common.dart';
|
import '../src/common.dart';
|
||||||
|
|
||||||
@ -26,8 +27,7 @@ void main() {
|
|||||||
expect(projectMetadata.versionChannel, isNull);
|
expect(projectMetadata.versionChannel, isNull);
|
||||||
expect(projectMetadata.versionRevision, isNull);
|
expect(projectMetadata.versionRevision, isNull);
|
||||||
|
|
||||||
expect(logger.traceText, contains('.metadata project_type version is malformed.'));
|
expect(logger.traceText, contains('No .metadata file found at .metadata'));
|
||||||
expect(logger.traceText, contains('.metadata version is malformed.'));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
testWithoutContext('project metadata fields are empty when file is empty', () {
|
testWithoutContext('project metadata fields are empty when file is empty', () {
|
||||||
@ -37,8 +37,7 @@ void main() {
|
|||||||
expect(projectMetadata.versionChannel, isNull);
|
expect(projectMetadata.versionChannel, isNull);
|
||||||
expect(projectMetadata.versionRevision, isNull);
|
expect(projectMetadata.versionRevision, isNull);
|
||||||
|
|
||||||
expect(logger.traceText, contains('.metadata project_type version is malformed.'));
|
expect(logger.traceText, contains('.metadata file at .metadata was empty or malformed.'));
|
||||||
expect(logger.traceText, contains('.metadata version is malformed.'));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
testWithoutContext('project metadata fields are empty when file is not valid yaml', () {
|
testWithoutContext('project metadata fields are empty when file is not valid yaml', () {
|
||||||
@ -48,8 +47,7 @@ void main() {
|
|||||||
expect(projectMetadata.versionChannel, isNull);
|
expect(projectMetadata.versionChannel, isNull);
|
||||||
expect(projectMetadata.versionRevision, isNull);
|
expect(projectMetadata.versionRevision, isNull);
|
||||||
|
|
||||||
expect(logger.traceText, contains('.metadata project_type version is malformed.'));
|
expect(logger.traceText, contains('.metadata file at .metadata was empty or malformed.'));
|
||||||
expect(logger.traceText, contains('.metadata version is malformed.'));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
testWithoutContext('projectType is populated when version is malformed', () {
|
testWithoutContext('projectType is populated when version is malformed', () {
|
||||||
@ -64,7 +62,7 @@ project_type: plugin
|
|||||||
expect(projectMetadata.versionChannel, isNull);
|
expect(projectMetadata.versionChannel, isNull);
|
||||||
expect(projectMetadata.versionRevision, 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', () {
|
testWithoutContext('version is populated when projectType is malformed', () {
|
||||||
@ -81,6 +79,94 @@ project_type: {}
|
|||||||
expect(projectMetadata.versionChannel, 'stable');
|
expect(projectMetadata.versionChannel, 'stable');
|
||||||
expect(projectMetadata.versionRevision, 'b59b226a49391949247e3d6122e34bb001049ae4');
|
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'));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -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, MigratePlatformConfig>{
|
||||||
|
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: <String>[
|
||||||
|
'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<SupportedPlatform> keyList = List<SupportedPlatform>.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'
|
||||||
|
'''));
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
flutter/test/full_app_fixtures/vanilla version:1.22.6_stable
|
@ -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<void> 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(<String>[
|
||||||
|
'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(<String>[
|
||||||
|
cipdFile.path,
|
||||||
|
'init',
|
||||||
|
tempDir.path,
|
||||||
|
'-force',
|
||||||
|
], workingDirectory: dir.path);
|
||||||
|
|
||||||
|
await processManager.run(<String>[
|
||||||
|
cipdFile.path,
|
||||||
|
'install',
|
||||||
|
'flutter/test/full_app_fixtures/vanilla',
|
||||||
|
version,
|
||||||
|
'-root',
|
||||||
|
tempDir.path,
|
||||||
|
], workingDirectory: dir.path);
|
||||||
|
|
||||||
|
if (Platform.isWindows) {
|
||||||
|
await processManager.run(<String>[
|
||||||
|
'robocopy',
|
||||||
|
tempDir.path,
|
||||||
|
dir.path,
|
||||||
|
'*',
|
||||||
|
'/E',
|
||||||
|
'/mov',
|
||||||
|
]);
|
||||||
|
// Add full access permissions to Users
|
||||||
|
await processManager.run(<String>[
|
||||||
|
'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(<String>[
|
||||||
|
'cp',
|
||||||
|
'-R',
|
||||||
|
'-L',
|
||||||
|
'-f',
|
||||||
|
'${tempDir.path}/.',
|
||||||
|
dir.path,
|
||||||
|
]);
|
||||||
|
|
||||||
|
await processManager.run(<String>[
|
||||||
|
'rm',
|
||||||
|
'-rf',
|
||||||
|
'.cipd',
|
||||||
|
], workingDirectory: dir.path);
|
||||||
|
|
||||||
|
final List<FileSystemEntity> allFiles = dir.listSync(recursive: true);
|
||||||
|
for (final FileSystemEntity file in allFiles) {
|
||||||
|
if (file is! File) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
await processManager.run(<String>[
|
||||||
|
'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
|
||||||
|
|
||||||
|
''';
|
||||||
|
}
|
@ -33,7 +33,7 @@ Directory createResolvedTempDirectorySync(String prefix) {
|
|||||||
void writeFile(String path, String content, {bool writeFutureModifiedDate = false}) {
|
void writeFile(String path, String content, {bool writeFutureModifiedDate = false}) {
|
||||||
final File file = fileSystem.file(path)
|
final File file = fileSystem.file(path)
|
||||||
..createSync(recursive: true)
|
..createSync(recursive: true)
|
||||||
..writeAsStringSync(content);
|
..writeAsStringSync(content, flush: true);
|
||||||
// Some integration tests on Windows to not see this file as being modified
|
// 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
|
// recently enough for the hot reload to pick this change up unless the
|
||||||
// modified time is written in the future.
|
// modified time is written in the future.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user