Visitar URL original
fix: stop `flutter create --platforms` from dropping existing platfor… by Shawn-Yu-Dev · Pull Request #191573 · flutter/flutter · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion packages/flutter_tools/lib/src/commands/create_base.dart
Original file line number Diff line number Diff line change
Expand Up @@ -546,12 +546,31 @@ mixin CreateBase on FlutterCommand {
final File metadataFile = globals.fs.file(
globals.fs.path.join(projectDir.absolute.path, '.metadata'),
);
// Seed the migrate config with the platforms already tracked in an
// existing .metadata file. Re-running `flutter create --platforms` must
// add the newly requested platforms without dropping the previously
// added ones, and without overwriting their recorded revisions.
// See https://github.com/flutter/flutter/issues/191567.

@Gustl22 Gustl22 Aug 25, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that comment is a bit confusing.
There is also this use case:

  • One adds a platform / multiple platforms, let's say: --platforms=android,ios
  • One recreates specific platform folders, to start from scratch (this was also common in packages repo to migrate to newer project standards, as not everything is migrated and/or additionally adds a new platform: --platforms=ios,linux

So I would recommend somethinng like:

      // ... Re-running `flutter create --platforms` must
      // add the platform without dropping or overwriting the previously
      // added ones, if they didn't exist yet.

Can we test the second case, too? So an entry for ios won't get duplicated (appended)?


var migrateConfig = MigrateConfig();
if (metadataFile.existsSync()) {
try {
migrateConfig = FlutterProjectMetadata(
metadataFile,
globals.logger,
extensionTemplateManager: null,
).migrateConfig;
} on Exception catch (error) {
globals.logger.printTrace('Failed to parse existing .metadata file: $error');
}
}

final metadata = FlutterProjectMetadata.explicit(
file: metadataFile,
versionRevision: globals.flutterVersion.frameworkRevision,
versionChannel: globals.flutterVersion.getBranchName(), // may contain PII
projectType: projectType,
migrateConfig: MigrateConfig(),
migrateConfig: migrateConfig,
logger: globals.logger,
extensionTemplateManager: null,
);
Expand Down
16 changes: 15 additions & 1 deletion packages/flutter_tools/lib/src/flutter_project_metadata.dart
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,11 @@ class MigrateConfig {

/// Parses the project for all supported platforms and populates the [MigrateConfig]
/// to reflect the project.
///
/// Platforms listed in [platforms] are the ones (re)generated by the current
/// operation. When a platform is already tracked and [create] is true, its
/// entry is overwritten with the current revisions instead of keeping stale
/// ones or adding a duplicate entry.
void populate({
List<SupportedPlatform>? platforms,
required Directory projectDirectory,
Expand All @@ -401,7 +406,16 @@ class MigrateConfig {

for (final SupportedPlatform platform in platforms) {
if (platformConfigs.containsKey(platform)) {
if (update) {
if (create) {
// The platform is being (re)created by this run: overwrite its
// entry so the recorded revisions reflect this run. Creating the
// same platform twice must not produce a duplicate entry.
platformConfigs[platform] = MigratePlatformConfig(
platform: platform,
createRevision: createRevision,
baseRevision: currentRevision,
);
} else if (update) {
platformConfigs[platform]!.baseRevision = currentRevision;
}
} else {
Expand Down
191 changes: 190 additions & 1 deletion packages/flutter_tools/test/commands.shard/permeable/create_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import 'package:flutter_tools/src/commands/create.dart';
import 'package:flutter_tools/src/commands/create_base.dart';
import 'package:flutter_tools/src/dart/pub.dart';
import 'package:flutter_tools/src/features.dart';
import 'package:flutter_tools/src/flutter_project_metadata.dart' show FlutterTemplateType;
import 'package:flutter_tools/src/flutter_project_metadata.dart';
import 'package:flutter_tools/src/globals.dart' as globals;
import 'package:flutter_tools/src/project.dart';
import 'package:flutter_tools/src/version.dart';
Expand Down Expand Up @@ -67,6 +67,24 @@ FakePlatform _kNoColorTerminalMacOSPlatform() => FakePlatform.fromPlatform(const
..stdoutSupportsAnsi = false
..operatingSystem = 'macos';

/// A [FakeFlutterVersion] whose [FakeFlutterVersion.frameworkRevision] can be
/// changed between `flutter create` runs, to simulate re-creating a platform
/// with a newer Flutter SDK revision.
class _MutableFakeFlutterVersion extends FakeFlutterVersion {
_MutableFakeFlutterVersion(String revision, String branch)
: _frameworkRevision = revision,
super(frameworkRevision: revision, branch: branch);

String _frameworkRevision;

@override
String get frameworkRevision => _frameworkRevision;

set frameworkRevision(String value) {
_frameworkRevision = value;
}
}

final Map<Type, FakePlatform Function()> noColorTerminalOverride = {
Platform: _kNoColorTerminalPlatform,
};
Expand Down Expand Up @@ -979,6 +997,100 @@ void main() {
expect(actualContents.contains('useAndroidX'), true);
});

testUsingContext('flutter create --platforms appends platforms to existing .metadata', () async {
// The revisions written to .metadata must look like git hashes; a purely
// numeric revision would be parsed back as a number by YAML and rejected
// by the migrate config validation.
final mutableFlutterVersion = _MutableFakeFlutterVersion(
'abcdef1234567890abcdef1234567890abcdef12',
frameworkChannel,
);
fakeFlutterVersion = mutableFlutterVersion;

final command = CreateCommand();
final CommandRunner<void> runner = createTestCommandRunner(command);

// Create the project with only the android platform enabled.
await runner.run(<String>['create', '--no-pub', '--platforms=android', projectDir.path]);

final File metadataFile = projectDir.childFile('.metadata');
expect(metadataFile.existsSync(), isTrue);

// .metadata is a YAML file, so read it back through FlutterProjectMetadata.
// Besides the requested platforms it always tracks a 'root' entry that
// records the revisions for the project itself.
final initialMetadata = FlutterProjectMetadata(
metadataFile,
globals.logger,
extensionTemplateManager: null,
);
const initialRevision = 'abcdef1234567890abcdef1234567890abcdef12';
expect(
initialMetadata.migrateConfig.platformConfigs.keys,
unorderedEquals(<SupportedPlatform>[SupportedPlatform.root, SupportedPlatform.android]),
);
expect(
initialMetadata.migrateConfig.platformConfigs[SupportedPlatform.android]!.createRevision,
initialRevision,
);
expect(
initialMetadata.migrateConfig.platformConfigs[SupportedPlatform.android]!.baseRevision,
initialRevision,
);

// Append the ios platform to the existing project, simulating a later
// run with a newer Flutter SDK revision. The platforms already recorded
// in the existing .metadata (android) must be preserved instead of being
// dropped.
// See https://github.com/flutter/flutter/issues/191567.
mutableFlutterVersion.frameworkRevision = 'fedcba9876543210fedcba9876543210fedcba98';
await runner.run(<String>['create', '--no-pub', '--platforms=ios', projectDir.path]);

final updatedMetadata = FlutterProjectMetadata(
metadataFile,
globals.logger,
extensionTemplateManager: null,
);
expect(
updatedMetadata.migrateConfig.platformConfigs.keys,
unorderedEquals(<SupportedPlatform>[
SupportedPlatform.root,
SupportedPlatform.android,
SupportedPlatform.ios,
]),
);
// Appending a new platform must not overwrite the revisions recorded for
// platforms that are not part of this run: the android entry keeps the
// revisions of the run that created it.
expect(
updatedMetadata.migrateConfig.platformConfigs[SupportedPlatform.android]!.createRevision,
initialRevision,
);
expect(
updatedMetadata.migrateConfig.platformConfigs[SupportedPlatform.android]!.baseRevision,
initialRevision,
);
// The root platform is part of every create run, so its entry reflects
// the most recent run, as does the newly added ios entry.
const latestRevision = 'fedcba9876543210fedcba9876543210fedcba98';
expect(
updatedMetadata.migrateConfig.platformConfigs[SupportedPlatform.root]!.createRevision,
latestRevision,
);
expect(
updatedMetadata.migrateConfig.platformConfigs[SupportedPlatform.root]!.baseRevision,
latestRevision,
);
expect(
updatedMetadata.migrateConfig.platformConfigs[SupportedPlatform.ios]!.createRevision,
latestRevision,
);
expect(
updatedMetadata.migrateConfig.platformConfigs[SupportedPlatform.ios]!.baseRevision,
latestRevision,
);
}, overrides: {FlutterVersion: () => fakeFlutterVersion});

testUsingContext('androidx is used by default in a module project', () async {
final command = CreateCommand();
final CommandRunner<void> runner = createTestCommandRunner(command);
Expand Down Expand Up @@ -1615,6 +1727,83 @@ void main() {
},
);

testUsingContext(
'flutter create --platforms overwrites the .metadata entry when the same platform is created twice',
() async {
// The revisions written to .metadata must look like git hashes; a
// purely numeric revision would be parsed back as a number by YAML and
// rejected by the migrate config validation.
final mutableFlutterVersion = _MutableFakeFlutterVersion(
'abcdef1234567890abcdef1234567890abcdef12',
frameworkChannel,
);
fakeFlutterVersion = mutableFlutterVersion;
final Directory tempDir = globals.fs.systemTempDirectory.createTempSync(
'flutter_create_overwrite_platform_',
);
final Directory projectDir = tempDir.childDirectory('myapp');

try {
// Create a project that supports the android platform.
final command = CreateCommand();
final CommandRunner<void> runner = createTestCommandRunner(command);
await runner.run(<String>['create', '--no-pub', '--platforms=android', projectDir.path]);

// Re-create the same android platform, simulating a later run with a
// newer Flutter SDK revision.
mutableFlutterVersion.frameworkRevision = 'fedcba9876543210fedcba9876543210fedcba98';
final repeatCommand = CreateCommand();
final CommandRunner<void> repeatRunner = createTestCommandRunner(repeatCommand);
await repeatRunner.run(<String>[
'create',
'--no-pub',
'--platforms=android',
projectDir.path,
]);

// Parse the migration section of the .metadata file with loadYaml,
// the same parser the tool uses when reading the file back.
final File metadataFile = projectDir.childFile('.metadata');
expect(metadataFile, exists);

final yamlMap = loadYaml(metadataFile.readAsStringSync()) as YamlMap;
final migration = yamlMap['migration'] as YamlMap;
final platforms = migration['platforms'] as YamlList;
final List<YamlMap> platformEntries = platforms.whereType<YamlMap>().toList();

// Each entry in the platforms list is a map that tracks the platform
// name along with the revisions it was created at; extract the names.
final actualPlatforms = <String>[
for (final YamlMap platform in platformEntries) platform['platform'] as String,
];

// Creating the same platform twice must not add a duplicate entry:
// the root platform and the android platform each appear exactly once.
expect(actualPlatforms, unorderedEquals(<String>['root', 'android']));

// The existing android entry is overwritten with the revisions of the
// most recent create run instead of keeping the old ones.
const latestRevision = 'fedcba9876543210fedcba9876543210fedcba98';
final YamlMap androidEntry = platformEntries.singleWhere(
(YamlMap platform) => platform['platform'] == 'android',
);
expect(androidEntry['create_revision'], latestRevision);
expect(androidEntry['base_revision'], latestRevision);

// The root platform is always part of every create run, so its entry
// is overwritten the same way.
final YamlMap rootEntry = platformEntries.singleWhere(
(YamlMap platform) => platform['platform'] == 'root',
);
expect(rootEntry['create_revision'], latestRevision);
expect(rootEntry['base_revision'], latestRevision);
} finally {
tryToDelete(tempDir);
}
},
overrides: {FlutterVersion: () => fakeFlutterVersion, Platform: _kNoColorTerminalPlatform},
);

testUsingContext('Correct info.plist key-value pairs for project.', () async {
final command = CreateCommand();
final CommandRunner<void> runner = createTestCommandRunner(command);
Expand Down