Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[flutter_migrate] Compute #2734

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 28 commits into from
Dec 6, 2022
Merged

[flutter_migrate] Compute #2734

merged 28 commits into from
Dec 6, 2022

Conversation

GaryQian
Copy link
Contributor

@GaryQian GaryQian commented Oct 24, 2022

Adds compute.dart which performs the bulk of the migrate tasks.

The migration is tracked as a MigrateContext which stores the MigrateResult as well as any intermediate state the migration needs.

The entry point is computeMigration(...) which begins the migration process. This is called by the start command which is the first command to be called by users.

As seen in the dart docs:

/// Computes the changes that migrates the current flutter project to the target revision.
///
/// This is the entry point to the core migration computations.
///
/// This method attempts to find a base revision, which is the revision of the Flutter SDK
/// the app was generated with or the last revision the app was migrated to. The base revision
/// typically comes from the .metadata, but for legacy apps, the config may not exist. In
/// this case, we fallback to using the revision in .metadata, and if that does not exist, we
/// use the target revision as the base revision. In the final fallback case, the migration should
/// still work, but will likely generate slightly less accurate merges.
///
/// Operations the computation performs:
///
///  - Parse .metadata file
///  - Collect revisions to use for each platform
///  - Download each flutter revision and call `flutter create` for each.
///  - Call `flutter create` with target revision (target is typically current flutter version)
///  - Diff base revision generated app with target revision generated app
///  - Compute all newly added files between base and target revisions
///  - Compute merge of all files that are modified by user and flutter
///  - Track temp dirs to be deleted
///
/// Structure: This method builds upon a MigrateResult instance

@GaryQian
Copy link
Contributor Author

This PR depends on #2723 for environment.dart but is otherwise independent

}
for (final String dir in _skippedDirectories) {
if (localPath.startsWith(
'${dir.replaceAll('/', fileSystem.path.separator)}${fileSystem.path.separator}')) {
Copy link
Contributor

Choose a reason for hiding this comment

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

isn't localPath absolute? so it would never start with, for example '.dart_tool', right? Or am I reading this wrong?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, in this PR, localPath is relative to the project root.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ahh, ok, I see you're right.

Copy link
Contributor

Choose a reason for hiding this comment

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

Similarly I recommend passing both _skippedDirectories and localPath through canonicalize() from package:path.

@christopherfujino
Copy link
Contributor

This code doesn't even compile for me:

~/git/packages/packages/flutter_migrate$ dart test test/compute_test.dart
00:01 +0 -1: loading test/compute_test.dart [E]
  Failed to load "test/compute_test.dart":
  test/compute_test.dart:62:72: Error: Too few positional arguments: 2 required, 1 given.
          await FlutterToolsEnvironment.initializeFlutterToolsEnvironment(logger);
                                                                         ^
  lib/src/environment.dart:31:42: Context: Found this candidate, but the arguments don't match.
    static Future<FlutterToolsEnvironment> initializeFlutterToolsEnvironment(
                                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

@GaryQian
Copy link
Contributor Author

GaryQian commented Nov 7, 2022

This is dependent on #2723, which just landed today. I should have just rebased it. Due to the review comments, I have changed the dependency API a bit, let me fix it real quick.

@christopherfujino
Copy link
Contributor

This is dependent on #2723, which just landed today. I should have just rebased it. Due to the review comments, I have changed the dependency API a bit, let me fix it real quick.

Ohh yeah sorry, you mentioned that

'lib', // Files here are always user owned and we don't want to overwrite their apps.
'test', // Files here are typically user owned and flutter-side changes are not relevant.
'assets', // Common directory for user assets.
'build' // Build artifacts
Copy link
Contributor

Choose a reason for hiding this comment

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

It would help readability to make the comments here all use the same form (consistency about capitalization and punctuation, consistency about whether to say "ignore the" given that's all ignoring, consistency about whether it's describing the files or the reason for excluding them).

Also, I would suggest keeping this list alphabetized so it's easier to see what's there.

];

bool _skipped(String localPath, FileSystem fileSystem,
{Set<String>? blacklistPrefixes}) {
Copy link
Contributor

Choose a reason for hiding this comment

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

skippedPrefixes

status.pause();
logger.printStatus('Obtaining revisions.',
indent: 2, color: TerminalColor.grey);
status.resume();
Copy link
Contributor

Choose a reason for hiding this comment

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

Having UI and core logic intermixed is going to make this hard to maintain over time (and to extend to other uses, like a GUI tool). I would strongly recommend having a state system that this communicates to, and have that state drive UI from completely separate code.

}
status.stop();
return context.migrateResult;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

This function is quite large, and doing a lot of things; extracting smaller, well-named helpers for individual parts would help with understanding the overall flow.

return '5391447fae6209bb21a89e6a5a6583cac1af9b4b';
}

abstract class MigrateFlutterProject {
Copy link
Contributor

Choose a reason for hiding this comment

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

This could use docs.

}
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Here too, consider extracting small helpers to allow someone to easily see the overall logical structure of the flow.

}
}

class MigrateTargetFlutterProject extends MigrateFlutterProject {
Copy link
Contributor

Choose a reason for hiding this comment

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

This could use docs explaining its role.

@@ -226,10 +226,10 @@ ${migrateConfig.getOutputFileString()}''';
/// 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,
{Map<SupportedPlatform?, MigratePlatformConfig>? platformConfigs,
Copy link
Contributor

Choose a reason for hiding this comment

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

how is a null key used here?

@@ -252,7 +252,7 @@ class MigrateConfig {
/// Parses the project for all supported platforms and populates the [MigrateConfig]
/// to reflect the project.
void populate({
List<SupportedPlatform>? platforms,
List<SupportedPlatform?>? platforms,
Copy link
Contributor

Choose a reason for hiding this comment

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

i guess same question here, is null a valid platform?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

See discussion below, changed to use a new FlutterProjectComponent enum that includes root as a value.

final List<SupportedPlatform> platforms = includeRoot
? <SupportedPlatform>[SupportedPlatform.root]
: <SupportedPlatform>[];
List<SupportedPlatform?> getSupportedPlatforms() {
Copy link
Contributor

Choose a reason for hiding this comment

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

It doesn't look like this should need to return a nullable generic.

this.unmanagedFiles = kDefaultUnmanagedFiles})
: platformConfigs =
platformConfigs ?? <SupportedPlatform, MigratePlatformConfig>{};
Copy link
Contributor

Choose a reason for hiding this comment

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

This should be writeable more simply as:

MigrateConfig({
  this.platformConfigs = const <...>{},
});

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}"}';
'\n - platform: ${entry.key == null ? 'root' : 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}"}';
Copy link
Contributor

Choose a reason for hiding this comment

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

This is still conflating "root" with platforms, just at a different level.

Stepping back a bit (since I'm not familiar with the high level flow), what is the thing that you are trying to represent here? Is it directories that make up a project, including both the root and the platform subdirectories?

Copy link
Contributor Author

@GaryQian GaryQian Nov 28, 2022

Choose a reason for hiding this comment

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

The purpose of this code is to track the Flutter git revision each project directory was flutter create-ed with. For this purpose only, the root directory is equivalent to any other platform-specific directory. It is equivalent because, like a platform directory, the root directory contains a subset of files that are created by the flutter create command. Upon creation, we log the flutter revision associated with the directory. Here, SupportedPlatform is a reference to directories within a project. As far as the migrate tool is concerned, a SupportedPlatform is just a directory, and it does not care about what the directory represents.

This tracking is to handle the case where the initial flutter create is called with, say 1.0.0, and then the windows platform is added using flutter 3.0. We would track different revisions for root+ios+android and windows.

Within the migrate tool, we also treat the root directory in the same way as platform directories. Once a project goes through a successful migration, we also track last successfully migrated revision in the same way.

Copy link
Contributor

Choose a reason for hiding this comment

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

It sounds like there should be a different class/enum for this that captures the concept then; something like FlutterProjectSection or FlutterProjectComponent. That way the confusion that comes from trying to express something other than a platform with SupportedPlatform is eliminated.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've added FlutterProjectComponent, and there are places in the code where it is appropriate to use SupportedPlatform and others where FlutterProjectComponent makes more sense. Having both does indeed seem like a more reasonable solution than conflating the two.

// We keep a spinner going and print periodic progress messages
// to assure the developer that the command is still working due to
// the long expected runtime.
Status? status;
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is this nullable? I'm not clear from reading this code what the distinction is between "never started" (null) and "started, but then stopped (non-null).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'll change this to initialize the spinner in the constructor

@GaryQian
Copy link
Contributor Author

@christopherfujino @stuartmorgan Comments addressed. Ready for another look.

Copy link
Contributor

@christopherfujino christopherfujino left a comment

Choose a reason for hiding this comment

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

LGTM

Copy link
Contributor

@stuartmorgan-g stuartmorgan-g left a comment

Choose a reason for hiding this comment

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

I left a few small comments, but otherwise looks good! I didn't do a full review, since you have one already, so I just skimmed (mostly around areas where I'd left comments previously) so my previous review won't block landing.

final Map<SupportedPlatform, MigratePlatformConfig> platformConfigs =
<SupportedPlatform, MigratePlatformConfig>{};
for (final MapEntry<SupportedPlatform, MigratePlatformConfig> entry
final Map<FlutterProjectComponent, MigratePlatformConfig> platformConfigs =
Copy link
Contributor

Choose a reason for hiding this comment

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

This should probably be called projectComponents, matching the type change.

<SupportedPlatform, MigratePlatformConfig>{};
for (final MapEntry<SupportedPlatform, MigratePlatformConfig> entry
final Map<FlutterProjectComponent, MigratePlatformConfig> platformConfigs =
<FlutterProjectComponent, MigratePlatformConfig>{};
Copy link
Contributor

Choose a reason for hiding this comment

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

Should the value be a Platform-related thing if the key is no longer platform-specific? Does that type need a name change or split as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, it does. But I think I'll leave a TODO here and do it in a separate PR to sync it here and in the flutter_tools repo. Id prefer to keep these concepts synced.

@@ -9,6 +9,37 @@ import 'base/file_system.dart';
import 'base/logger.dart';
import 'base/project.dart';

enum FlutterProjectComponent {
Copy link
Contributor

Choose a reason for hiding this comment

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

This should have a documentation comment.

}

extension FlutterProjectComponentExtension on FlutterProjectComponent {
SupportedPlatform? toSupportedPlatform() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe remove this? It doesn't appear to be used anywhere, and it's a remnant of conflating components and platforms. Code using this seems like it would be suspect for the same reason the earlier design was problematic.

// final String canonicalizedLocalPath = canonicalize(localPath);
// if (canonicalizedSkippedFiles.contains(canonicalizedLocalPath)) {
// return true;
// }
Copy link
Contributor

Choose a reason for hiding this comment

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

Remove? We generally don't want to check in commented-out code, especially not without a comment explaining it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oops, missed this in the cleanup.

/// False for files that should not be merged. Typically, images and binary files.
bool _mergable(String localPath) {
for (final String ext in _doNotMergeFileExtensions) {
if (localPath.endsWith(ext) && !_alwaysMigrateFiles.contains(localPath)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

There's no need to re-check _alwaysMigrateFiles in the loop, since that expression doesn't use ext.

It looks like this whole function could simplify to:

return _alwaysMigrateFiles.contains(localPath) || ! _doNotMergeFileExtensions.any((String ext) => localPath.endsWith(ext));

skippedPrefixes.add(platformToSubdirectoryPrefix(platform));
}
for (final SupportedPlatform platform in platforms) {
if (platform != null) {
Copy link
Contributor

Choose a reason for hiding this comment

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

This can't happen any more; it's a non-nullable type now.

@GaryQian GaryQian added the autosubmit Merge PR when tree becomes green via auto submit App label Dec 6, 2022
@auto-submit auto-submit bot merged commit 57e388d into flutter:main Dec 6, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
autosubmit Merge PR when tree becomes green via auto submit App p: flutter_migrate
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants